poetry-core 0.1.3 → 0.1.5
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/.controller_docs +1 -0
- data/.jsdoc_floor +1 -0
- data/.mcp.json +8 -0
- data/.yard-lint.yml +28 -0
- data/.yard_coverage_all +1 -0
- data/Archspec.rb +22 -0
- data/CHANGELOG.md +23 -0
- data/app/components/poetry/core/component.rb +98 -6
- data/app/components/poetry/core/concerns/agent_tools.rb +6 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +1 -1
- data/app/components/poetry/core/concerns/introspection.rb +67 -36
- data/app/components/poetry/core/concerns/parts.rb +4 -0
- data/app/components/poetry/core/concerns/stimulus.rb +4 -0
- data/app/components/poetry/core/concerns/styles.rb +1 -1
- data/app/javascript/poetry/core/accordion_controller.js +10 -7
- data/app/javascript/poetry/core/action_bar_controller.js +12 -8
- data/app/javascript/poetry/core/autocomplete_controller.js +15 -11
- data/app/javascript/poetry/core/calendar_controller.js +36 -29
- data/app/javascript/poetry/core/carousel_controller.js +13 -9
- data/app/javascript/poetry/core/checkbox_group_controller.js +19 -17
- data/app/javascript/poetry/core/checked_controller.js +32 -28
- data/app/javascript/poetry/core/clipboard_text_controller.js +10 -8
- data/app/javascript/poetry/core/combobox_controller.js +53 -47
- data/app/javascript/poetry/core/command_controller.js +35 -37
- data/app/javascript/poetry/core/context_menu_controller.js +28 -24
- data/app/javascript/poetry/core/date_field_controller.js +22 -16
- data/app/javascript/poetry/core/date_picker_controller.js +11 -6
- data/app/javascript/poetry/core/deferred_controller.js +26 -21
- data/app/javascript/poetry/core/dialog_controller.js +11 -9
- data/app/javascript/poetry/core/dismissable_controller.js +9 -5
- data/app/javascript/poetry/core/drawer_controller.js +24 -21
- data/app/javascript/poetry/core/file_input_controller.js +14 -11
- data/app/javascript/poetry/core/focus_scope_controller.js +11 -7
- data/app/javascript/poetry/core/hotkey_controller.js +15 -11
- data/app/javascript/poetry/core/hover_card_controller.js +25 -19
- data/app/javascript/poetry/core/mask_controller.js +42 -33
- data/app/javascript/poetry/core/menu_controller.js +40 -32
- data/app/javascript/poetry/core/menubar_controller.js +34 -30
- data/app/javascript/poetry/core/message_scroller_controller.js +29 -1
- data/app/javascript/poetry/core/navigation_menu_controller.js +27 -22
- data/app/javascript/poetry/core/number_field_controller.js +23 -11
- data/app/javascript/poetry/core/optimistic_form_controller.js +20 -18
- data/app/javascript/poetry/core/otp_controller.js +26 -22
- data/app/javascript/poetry/core/popover_controller.js +26 -22
- data/app/javascript/poetry/core/popper_controller.js +33 -27
- data/app/javascript/poetry/core/pressed_controller.js +15 -13
- data/app/javascript/poetry/core/questionnaire_controller.js +19 -13
- data/app/javascript/poetry/core/radio_group_controller.js +21 -18
- data/app/javascript/poetry/core/resizable_controller.js +11 -8
- data/app/javascript/poetry/core/roving_focus_controller.js +16 -13
- data/app/javascript/poetry/core/scroll_spy_controller.js +14 -12
- data/app/javascript/poetry/core/search_field_controller.js +10 -8
- data/app/javascript/poetry/core/select_controller.js +34 -24
- data/app/javascript/poetry/core/sensitive_input_controller.js +16 -14
- data/app/javascript/poetry/core/sheet_controller.js +10 -8
- data/app/javascript/poetry/core/sidebar_controller.js +23 -18
- data/app/javascript/poetry/core/slider_controller.js +37 -28
- data/app/javascript/poetry/core/state_controller.js +16 -11
- data/app/javascript/poetry/core/table_selection_controller.js +20 -16
- data/app/javascript/poetry/core/tabs_controller.js +17 -15
- data/app/javascript/poetry/core/tag_group_controller.js +21 -19
- data/app/javascript/poetry/core/toast_controller.js +20 -15
- data/app/javascript/poetry/core/toast_trigger_controller.js +7 -5
- data/app/javascript/poetry/core/toaster_controller.js +20 -15
- data/app/javascript/poetry/core/toggle_group_controller.js +21 -18
- data/app/javascript/poetry/core/tooltip_controller.js +23 -18
- data/app/javascript/poetry/core/tree_controller.js +14 -12
- data/config/component_registry.yml +33 -0
- data/config/controllers_manifest.json +792 -190
- data/eslint.config.mjs +35 -0
- data/lib/poetry/core/api_internals.rb +102 -0
- data/lib/poetry/core/check/stable_identity.rb +3 -0
- data/lib/poetry/core/check.rb +189 -53
- data/lib/poetry/core/config.rb +3 -11
- data/lib/poetry/core/contrib/wrapped_helper.rb +1 -2
- data/lib/poetry/core/css/bem_reference.rb +8 -0
- data/lib/poetry/core/css/modes.rb +4 -0
- data/lib/poetry/core/css/override_scan.rb +8 -0
- data/lib/poetry/core/css/resolver.rb +8 -0
- data/lib/poetry/core/css/safelist.rb +3 -0
- data/lib/poetry/core/css/tailwind_merger.rb +1 -2
- data/lib/poetry/core/css/template_classes.rb +6 -0
- data/lib/poetry/core/css/theme_coverage.rb +4 -0
- data/lib/poetry/core/css/token_collisions.rb +6 -0
- data/lib/poetry/core/css/var_coverage.rb +6 -0
- data/lib/poetry/core/css/verifier.rb +5 -0
- data/lib/poetry/core/design_lint.rb +57 -0
- data/lib/poetry/core/design_md/import.rb +16 -0
- data/lib/poetry/core/design_md.rb +29 -0
- data/lib/poetry/core/errors.rb +2 -0
- data/lib/poetry/core/host_components.rb +9 -0
- data/lib/poetry/core/host_helpers.rb +5 -0
- data/lib/poetry/core/html/attributes.rb +1 -1
- data/lib/poetry/core/icons.rb +3 -1
- data/lib/poetry/core/llms_text.rb +14 -11
- data/lib/poetry/core/page_architectures.rb +2 -0
- data/lib/poetry/core/part_contract.rb +21 -0
- data/lib/poetry/core/preview/abstract.rb +3 -2
- data/lib/poetry/core/preview/base.rb +3 -2
- data/lib/poetry/core/preview/sidecarable.rb +2 -2
- data/lib/poetry/core/preview/template.rb +4 -3
- data/lib/poetry/core/recipe_items.rb +5 -0
- data/lib/poetry/core/registry.rb +93 -50
- data/lib/poetry/core/registry_client.rb +15 -0
- data/lib/poetry/core/registry_installer.rb +14 -0
- data/lib/poetry/core/registry_items.rb +9 -0
- data/lib/poetry/core/requires_any.rb +25 -0
- data/lib/poetry/core/skill_text.rb +8 -0
- data/lib/poetry/core/stable_id.rb +3 -0
- data/lib/poetry/core/stimulus/builder.rb +2 -1
- data/lib/poetry/core/stimulus/declarations.rb +10 -5
- data/lib/poetry/core/stimulus/host_manifest.rb +19 -12
- data/lib/poetry/core/stimulus_contract.rb +16 -0
- data/lib/poetry/core/template_compile.rb +4 -0
- data/lib/poetry/core/token_import.rb +10 -0
- data/lib/poetry/core/tokens/color.rb +2 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +6 -0
- data/lib/poetry/core/tokens/generator.rb +6 -0
- data/lib/poetry/core/version.rb +1 -1
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af029ac85cadd2b3906f885dc1123ca58a73bcd55ffe07041abde3933a5a08f4
|
|
4
|
+
data.tar.gz: 3c5efbc74950c59708605ee7d77371d4c1b3f47e3fefc661042cfe9ce4179a34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4a190f5aec965c7defb6ce6005cf5ba388f393ee7955fd72e8ccc9d615442f7d37e7e105a7e51380826e5c7c9d22d10a5ca50400bd7a9da23bc8068be207c30
|
|
7
|
+
data.tar.gz: 9487e4788dac96cc7bc8212cb519c0dfd6d7e6ebdab6e34f7ba6fd2fc598c6bb00905974bb9370692d84ee01cf103f339eabc94884d9f45372ebedccfce33913
|
data/.controller_docs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0
|
data/.jsdoc_floor
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
605
|
data/.mcp.json
ADDED
data/.yard-lint.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# yard-lint: the documentation tier YARD's own gates miss - a documented
|
|
2
|
+
# method missing a @param, an @example that does not parse, option tags,
|
|
3
|
+
# tag order, invalid types. Runs as rake yard:lint (in the default task).
|
|
4
|
+
# UndocumentedObjects stays off: yard:coverage gates public objects at its
|
|
5
|
+
# recorded floor, and this validator counts the @api private internals the
|
|
6
|
+
# gems hide on purpose (--hide-api is not honoured here). MissingReturn is
|
|
7
|
+
# off until the @param backlog is cleared; revisit then.
|
|
8
|
+
AllValidators:
|
|
9
|
+
YardOptions:
|
|
10
|
+
- --no-private
|
|
11
|
+
- --load
|
|
12
|
+
- yard/poetry_yard.rb
|
|
13
|
+
|
|
14
|
+
Documentation/UndocumentedObjects:
|
|
15
|
+
Enabled: false
|
|
16
|
+
Documentation/MissingReturn:
|
|
17
|
+
Enabled: false
|
|
18
|
+
Tags/InvalidTypes:
|
|
19
|
+
Enabled: true
|
|
20
|
+
# A **options splat passed through to a control is documented as @param
|
|
21
|
+
# options [Hash], not one @option per key; and @example blocks here are
|
|
22
|
+
# ERB as often as Ruby, which this validator cannot parse.
|
|
23
|
+
Documentation/UndocumentedOptions:
|
|
24
|
+
Enabled: false
|
|
25
|
+
Tags/OptionTags:
|
|
26
|
+
Enabled: false
|
|
27
|
+
Tags/ExampleSyntax:
|
|
28
|
+
Enabled: false
|
data/.yard_coverage_all
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0
|
data/Archspec.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The architecture the family enforces by review, as checks (rake arch:check).
|
|
4
|
+
# Core is the root of the family: nothing here names a sibling gem or the
|
|
5
|
+
# host application, and lib reaches the components only through the
|
|
6
|
+
# registry and the base class.
|
|
7
|
+
root "."
|
|
8
|
+
source "app/**/*.rb", "lib/**/*.rb"
|
|
9
|
+
|
|
10
|
+
component :kernel, constants: %w[Poetry::Core::Component Poetry::Core::Wrapper::Component]
|
|
11
|
+
component :components, in: "app/components/**/*.rb",
|
|
12
|
+
except: ["app/components/poetry/core/component.rb", "app/components/poetry/core/wrapper/**/*"]
|
|
13
|
+
component :lib, in: "lib/**/*.rb"
|
|
14
|
+
|
|
15
|
+
lib.cannot_use :components, because: "lib reaches the components through the registry and the base class"
|
|
16
|
+
lib.cannot_reference_constants "Poetry::Ui", "Poetry::Charts", "Poetry::Agent", "Poetry::Extract",
|
|
17
|
+
"ApplicationController",
|
|
18
|
+
because: "core is the root of the family and never names the host"
|
|
19
|
+
components.cannot_reference_constants "Poetry::Ui", "Poetry::Charts", "Poetry::Agent", "Poetry::Extract",
|
|
20
|
+
"ApplicationController",
|
|
21
|
+
because: "core is the root of the family and never names the host"
|
|
22
|
+
no_cycles among: %i[lib components]
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.5] - 2026-09-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- The controllers manifest carries the prose beside each controller: its purpose (a JSDoc block above the class), the meaning of every value, and a summary of every action method, harvested when the manifest is generated; `rake stimulus:docs` holds the count of gaps at a committed floor, now zero.
|
|
8
|
+
- `poetry check` warns when app Ruby or a template names one of the family's internal namespaces (`internal-constant`): a class or module the reference hides with `@api private` works today and may change without notice. The registries carry the list (`internals`, read from source by `Poetry::Core::ApiInternals` at generation), so the check, the MCP `check` tool and a host's editor hook all read the same contract without booting; core's own internals join every catalog.
|
|
9
|
+
- `Poetry::Core::Component#root_attributes`, the root element's attributes ready to splat: the caller's `html_attributes` (classes already merged with the dictionary's) over the component's own root markup - `data-slot` (`root_slot`, the component title in kebab form, or the one passed), `data-component`, and the root's Stimulus wiring whenever a `use_stimulus` block declares `:root`. A component adds its markup by overriding and passing it up (`super("role" => "status")`); the template writes `tag.div(**root_attributes)`. The private `root_attributes` every component and the guide's sample wrote by hand is the default now.
|
|
10
|
+
- `Poetry::Core::Component#element_attributes(part, markup, stimulus:)`, the builder for a part's attributes, ready to splat: the part's `data-slot` (the root slot and the part name), the dictionary's classes for it when the Style declares the element, the part's Stimulus wiring when a `use_stimulus` block declares an element of that name, and the markup over all three (its own `"data-slot"` or `"class"` wins; `stimulus:` names another element, or `false` for none). The wiring merges the safe way: plain `Hash#merge` of markup with wiring dropped one side's `data-controller` or `data-action` when both carried one, and this concatenates them. Build every part here and write `tag.div(**content_attributes)`; `to_attributes` leaves the templates.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- A message scroller opening at the end (or the last anchor) no longer settles at the top of the thread when its controller connects before the page's render-blocking stylesheet lands. The opening position counted as applied against a viewport that had nothing to scroll yet; it now finalizes only against a laid-out viewport, else on the first resize pass, and the opening hold (`data-pending-scroll`) releases there.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- 133 methods the reference already hid with `@api private` are Ruby-private now: each was called only by its own class or template, so the runtime enforces what the tag only stated. A host that reached one gets a NoMethodError instead of an internal that may change without notice. The tag remains on the internals the family shares between its gems and on whole internal classes.
|
|
19
|
+
- Every class, module and method carries a one-sentence description, private helpers included: `rake yard:coverage:all` measures the whole tree (a tag-only docstring counts as blank) and the committed floor now stands at zero.
|
|
20
|
+
- `Poetry::Core::RequiresAny.phrase` states an any-of contract once for `poetry check`, llms.txt and the MCP server; the registry entry builder, the slot surface builder and the host manifest's literal scanner are split into named steps with no change in output (the committed registries rebuild byte for byte).
|
|
21
|
+
|
|
22
|
+
## [0.1.4] - 2026-09-15
|
|
23
|
+
|
|
24
|
+
Lockstep release with the family; no changes in this gem.
|
|
25
|
+
|
|
3
26
|
## [0.1.3] - 2026-09-13
|
|
4
27
|
|
|
5
28
|
### Fixed
|
|
@@ -62,6 +62,11 @@ module Poetry
|
|
|
62
62
|
# Button's render when it ran through `valid?`.
|
|
63
63
|
class_attribute :declared_values, instance_accessor: false, default: {}.freeze
|
|
64
64
|
|
|
65
|
+
# The caller-supplied semantic identity (key:), if any.
|
|
66
|
+
#
|
|
67
|
+
# @return [Object, nil]
|
|
68
|
+
attr_reader :stable_key
|
|
69
|
+
|
|
65
70
|
class << self
|
|
66
71
|
# Marks this class (and its descendants) as an implementation
|
|
67
72
|
# detail - full machinery, no registry entry.
|
|
@@ -116,8 +121,9 @@ module Poetry
|
|
|
116
121
|
# @param name [Symbol]
|
|
117
122
|
# @param variants [Array, nil] the closed vocabulary, nil for open values
|
|
118
123
|
# @param required [Boolean]
|
|
124
|
+
# @param open [Boolean] a style redeclared without variants: any value passes,
|
|
125
|
+
# the vocabulary is not enforced
|
|
119
126
|
# @return [void]
|
|
120
|
-
# @api private
|
|
121
127
|
def record_declared_value(name, variants:, required:, open: false)
|
|
122
128
|
spec = { variants: variants, required: required, open: open }.freeze
|
|
123
129
|
self.declared_values = declared_values.merge(name.to_sym => spec).freeze
|
|
@@ -274,6 +280,94 @@ module Poetry
|
|
|
274
280
|
{ "data-slot" => part.to_s }
|
|
275
281
|
end
|
|
276
282
|
|
|
283
|
+
# The root element's attributes, ready to splat: the caller's
|
|
284
|
+
# {#html_attributes} (its classes already merged with the dictionary's)
|
|
285
|
+
# over the component's own root markup - `data-slot` (the component
|
|
286
|
+
# title, or the one in +extra+), `data-component`, and the root's
|
|
287
|
+
# Stimulus wiring when a `use_stimulus` block declares `:root`. The
|
|
288
|
+
# caller's attributes win: a class, id or data-* passed in is kept and
|
|
289
|
+
# the component's default fills the gaps.
|
|
290
|
+
#
|
|
291
|
+
# A component adds its own root markup by overriding and passing it up;
|
|
292
|
+
# the template splats the result.
|
|
293
|
+
#
|
|
294
|
+
# @example A component's override
|
|
295
|
+
# def root_attributes
|
|
296
|
+
# super("role" => "status", "data-variant" => variant)
|
|
297
|
+
# end
|
|
298
|
+
# @example The template
|
|
299
|
+
# <%= tag.div(**root_attributes) do %>
|
|
300
|
+
#
|
|
301
|
+
# @param extra [Hash] the component's own root attributes; a
|
|
302
|
+
# `"data-slot"` here replaces the default
|
|
303
|
+
# @return [Hash] the root's attributes, flat (`data-x`, `aria-x`,
|
|
304
|
+
# booleans as the attribute name), for `tag` and `content_tag`
|
|
305
|
+
def root_attributes(extra = {})
|
|
306
|
+
own = { "data-slot" => root_slot }.merge(extra).merge(component_data_attributes)
|
|
307
|
+
wired = self.class.stimulus_elements.key?(:root) ? :root : nil
|
|
308
|
+
html_attributes.merge_if_not_set(element_markup(own, stimulus: wired)).to_attributes
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# The root's `data-slot`: the component title in kebab form (a
|
|
312
|
+
# ToggleGroup roots as "toggle-group").
|
|
313
|
+
#
|
|
314
|
+
# @return [String] the slot name
|
|
315
|
+
def root_slot
|
|
316
|
+
self.class.component_title.to_s.tr("_", "-")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# A part's attributes, ready to splat. Named after a part, it carries
|
|
320
|
+
# the part's `data-slot` (the root slot and the part name: a
|
|
321
|
+
# DropdownMenu's `:content` is "dropdown-menu-content"), the
|
|
322
|
+
# dictionary's classes for it (`css(part)`, when the Style declares the
|
|
323
|
+
# element), and the part's Stimulus wiring when a `use_stimulus` block
|
|
324
|
+
# declares an element of that name - each of which the markup may
|
|
325
|
+
# override (its own `"data-slot"` or `"class"` wins; `stimulus:` names
|
|
326
|
+
# another element, or `false` for none). The wiring merges beneath the
|
|
327
|
+
# markup the safe way: plain `Hash#merge` drops one side's
|
|
328
|
+
# `data-controller` or `data-action` when both carry one, and this
|
|
329
|
+
# concatenates them. Rendered more than once (a row, an addon), the
|
|
330
|
+
# builder takes its arguments and the part stays the same.
|
|
331
|
+
#
|
|
332
|
+
# @example A part builder, and its template
|
|
333
|
+
# def content_attributes
|
|
334
|
+
# attrs = { "id" => content_id, "role" => "menu" }
|
|
335
|
+
# attrs["hidden"] = true unless open
|
|
336
|
+
# element_attributes(:content, attrs)
|
|
337
|
+
# end
|
|
338
|
+
# # <%= tag.div(**content_attributes) do %>
|
|
339
|
+
#
|
|
340
|
+
# @param part [Symbol, String, nil] the part; nil (or a Hash in its
|
|
341
|
+
# place) for markup with no part of its own
|
|
342
|
+
# @param attrs [Hash] the element's markup, in braces after a part
|
|
343
|
+
# @param stimulus [Symbol, String, false, nil] the `use_stimulus`
|
|
344
|
+
# element whose wiring joins the markup: nil takes the part's own when
|
|
345
|
+
# one is declared, false takes none
|
|
346
|
+
# @return [Hash] the element's attributes, flat, for `tag` and `content_tag`
|
|
347
|
+
def element_attributes(part = nil, attrs = {}, stimulus: nil)
|
|
348
|
+
if part.is_a?(Hash)
|
|
349
|
+
attrs = part
|
|
350
|
+
part = nil
|
|
351
|
+
end
|
|
352
|
+
own = attrs
|
|
353
|
+
if part
|
|
354
|
+
own = { "data-slot" => "#{root_slot}-#{part.to_s.tr("_", "-")}", "class" => css(part) }.compact.merge(attrs)
|
|
355
|
+
stimulus = part.to_sym if stimulus.nil? && self.class.stimulus_elements.key?(part.to_sym)
|
|
356
|
+
end
|
|
357
|
+
element_markup(own, stimulus: stimulus || nil).to_attributes
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# The merge-aware form of an element's markup with its wiring.
|
|
361
|
+
#
|
|
362
|
+
# @param attrs [Hash] the element's markup
|
|
363
|
+
# @param stimulus [Symbol, String, nil] a declared element, or nil
|
|
364
|
+
# @return [Poetry::Core::HTML::Attributes]
|
|
365
|
+
def element_markup(attrs, stimulus:)
|
|
366
|
+
attributes = Poetry::Core::HTML::Attributes.new(attrs)
|
|
367
|
+
stimulus ? attributes.merge(stimulus_attributes_for(stimulus)) : attributes
|
|
368
|
+
end
|
|
369
|
+
private :element_markup
|
|
370
|
+
|
|
277
371
|
# Enforces the class-level requires_content declaration - call from
|
|
278
372
|
# before_render. The message is built from the declaration so the
|
|
279
373
|
# runtime raise and the registry's static contract can never disagree.
|
|
@@ -469,6 +563,7 @@ module Poetry
|
|
|
469
563
|
"#{allowed.map(&:inspect).join(", ")}#{variant_suggestion(value, allowed)}"
|
|
470
564
|
end
|
|
471
565
|
|
|
566
|
+
# The did-you-mean suffix for an off-list value, or an empty string.
|
|
472
567
|
def variant_suggestion(value, allowed)
|
|
473
568
|
return "" if allowed.empty? || value.nil?
|
|
474
569
|
|
|
@@ -527,11 +622,6 @@ module Poetry
|
|
|
527
622
|
attributes
|
|
528
623
|
end
|
|
529
624
|
|
|
530
|
-
# The caller-supplied semantic identity (key:), if any.
|
|
531
|
-
#
|
|
532
|
-
# @return [Object, nil]
|
|
533
|
-
attr_reader :stable_key
|
|
534
|
-
|
|
535
625
|
# The instance-id ladder: an explicit caller root id wins; a key:
|
|
536
626
|
# derives a stable component-namespaced token (Turbo morph pairs it
|
|
537
627
|
# across renders, cached fragments stay composable); otherwise
|
|
@@ -624,6 +714,8 @@ module Poetry
|
|
|
624
714
|
controller.request = ActionDispatch::TestRequest.create
|
|
625
715
|
render_in(controller.view_context)
|
|
626
716
|
end
|
|
717
|
+
|
|
718
|
+
private_class_method :record_declared_value
|
|
627
719
|
end
|
|
628
720
|
end
|
|
629
721
|
end
|
|
@@ -218,6 +218,7 @@ module Poetry
|
|
|
218
218
|
words[0].upcase + words[1..]
|
|
219
219
|
end
|
|
220
220
|
|
|
221
|
+
# Raises unless the tool name is a symbol or string in the allowed shape and length.
|
|
221
222
|
# @api private
|
|
222
223
|
def validate_tool_name!(name)
|
|
223
224
|
unless name.is_a?(Symbol) || name.is_a?(String)
|
|
@@ -233,6 +234,7 @@ module Poetry
|
|
|
233
234
|
text.to_sym
|
|
234
235
|
end
|
|
235
236
|
|
|
237
|
+
# The description trimmed, raising when it is missing or too long.
|
|
236
238
|
# @api private
|
|
237
239
|
def validate_tool_description!(name, description)
|
|
238
240
|
text = description.to_s.strip
|
|
@@ -269,6 +271,7 @@ module Poetry
|
|
|
269
271
|
end
|
|
270
272
|
end
|
|
271
273
|
|
|
274
|
+
# The tool's input schema as string-keyed JSON Schema, from params: or input_schema: but never both.
|
|
272
275
|
# @api private
|
|
273
276
|
def normalize_tool_schema!(name, params, input_schema)
|
|
274
277
|
if params && input_schema
|
|
@@ -301,6 +304,7 @@ module Poetry
|
|
|
301
304
|
schema
|
|
302
305
|
end
|
|
303
306
|
|
|
307
|
+
# Raises when a parameter's description is blank or too long.
|
|
304
308
|
# @api private
|
|
305
309
|
def validate_param_description!(name, param, description)
|
|
306
310
|
return if description.nil?
|
|
@@ -313,6 +317,7 @@ module Poetry
|
|
|
313
317
|
"and at most #{PARAM_DESCRIPTION_LIMIT} characters"
|
|
314
318
|
end
|
|
315
319
|
|
|
320
|
+
# The input schema as given, raising unless it is a hash with a type.
|
|
316
321
|
# @api private
|
|
317
322
|
def validate_input_schema!(name, input_schema)
|
|
318
323
|
unless input_schema.is_a?(Hash) && (input_schema[:type] || input_schema["type"])
|
|
@@ -323,6 +328,7 @@ module Poetry
|
|
|
323
328
|
input_schema
|
|
324
329
|
end
|
|
325
330
|
|
|
331
|
+
# The value with every hash key and symbol turned into a string, recursively.
|
|
326
332
|
# @api private
|
|
327
333
|
def deep_stringify_tool_keys(value)
|
|
328
334
|
case value
|
|
@@ -62,7 +62,7 @@ module Poetry
|
|
|
62
62
|
# stays in the hash for `attribute` to install.
|
|
63
63
|
#
|
|
64
64
|
# @param options [Hash] the original options hash
|
|
65
|
-
# @return [Array
|
|
65
|
+
# @return [Array(Boolean, Object)] required and default_value
|
|
66
66
|
def extract_declared_defaults(options)
|
|
67
67
|
default_value = options[:default]
|
|
68
68
|
options.delete(:default) if default_value.is_a?(Proc)
|
|
@@ -139,6 +139,7 @@ module Poetry
|
|
|
139
139
|
callable || renders || (opts unless opts.empty?)
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
+
# One style attribute's registry definition: type, variants, default, required and doc.
|
|
142
143
|
def style_definition(name)
|
|
143
144
|
definition = { name: name, type: attribute_types[name.to_s].type }
|
|
144
145
|
variants = respond_to?("#{name}_variants") ? public_send("#{name}_variants") : nil
|
|
@@ -151,6 +152,7 @@ module Poetry
|
|
|
151
152
|
definition
|
|
152
153
|
end
|
|
153
154
|
|
|
155
|
+
# One option's registry definition: type, the inclusion validator as its enum, default, required and doc.
|
|
154
156
|
def option_definition(name)
|
|
155
157
|
definition = { name: name, type: attribute_types[name.to_s].type }
|
|
156
158
|
# An inclusion validator IS the option's enum contract:
|
|
@@ -203,6 +205,7 @@ module Poetry
|
|
|
203
205
|
end
|
|
204
206
|
end
|
|
205
207
|
|
|
208
|
+
# Whether a presence validator makes the attribute required.
|
|
206
209
|
def required_attribute?(name)
|
|
207
210
|
validators_on(name).any? { |validator| validator.kind == :presence }
|
|
208
211
|
end
|
|
@@ -282,42 +285,14 @@ module Poetry
|
|
|
282
285
|
def slot_surface(klass, seen: [])
|
|
283
286
|
return [] unless klass.respond_to?(:registered_slots)
|
|
284
287
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
definition[:description] = doc if doc
|
|
294
|
-
renderable = config[:renderable]
|
|
295
|
-
definition[:component] = renderable.component_path if renderable.respond_to?(:component_path)
|
|
296
|
-
# A declared pure-forwarding lambda (SLOT_RENDERS) restores
|
|
297
|
-
# the component fact a wrapping lambda hides.
|
|
298
|
-
# Polymorphic slots stay out - their types are their contract.
|
|
299
|
-
if config[:renderable_hash].nil? &&
|
|
300
|
-
(declared = renders[slot_setters(slot_name, config).first&.to_sym])
|
|
301
|
-
unless declared.respond_to?(:component_path)
|
|
302
|
-
raise Poetry::Core::Error,
|
|
303
|
-
"#{klass}::SLOT_RENDERS[#{slot_name}] must be a poetry component class"
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
definition[:component] ||= declared.component_path
|
|
307
|
-
end
|
|
308
|
-
definition[:types] = config[:renderable_hash].keys if config[:renderable_hash]
|
|
309
|
-
setter_args = setter_positional_args(slot_name, config)
|
|
310
|
-
definition[:setter_args] = setter_args unless setter_args.empty?
|
|
311
|
-
setter_kwargs = setter_keyword_args(slot_name, config)
|
|
312
|
-
definition[:setter_kwargs] = setter_kwargs unless setter_kwargs.empty?
|
|
313
|
-
yieldless = yieldless_setters(slot_name, config) - block_yields.keys
|
|
314
|
-
definition[:yieldless] = yieldless unless yieldless.empty?
|
|
315
|
-
required = required_content.slice(*slot_setters(slot_name, config).map(&:to_sym))
|
|
316
|
-
definition[:required_content] = required unless required.empty?
|
|
317
|
-
surfaces = builder_surfaces(slot_name, config, builders, seen + [klass])
|
|
318
|
-
definition[:builders] = surfaces unless surfaces.empty?
|
|
319
|
-
definition
|
|
320
|
-
end
|
|
288
|
+
context = {
|
|
289
|
+
klass: klass, seen: seen + [klass],
|
|
290
|
+
builders: declared_builders(klass), required_content: declared_required_content(klass),
|
|
291
|
+
block_yields: declared_constant(klass, :SLOT_BLOCK_YIELDS),
|
|
292
|
+
renders: declared_constant(klass, :SLOT_RENDERS),
|
|
293
|
+
slot_docs: klass.respond_to?(:slot_docs) ? klass.slot_docs : {}
|
|
294
|
+
}
|
|
295
|
+
klass.registered_slots.map { |slot_name, config| slot_definition(slot_name, config, context) }
|
|
321
296
|
end
|
|
322
297
|
|
|
323
298
|
# The validated REQUIRED_SLOTS declaration of a slot-owning class:
|
|
@@ -399,14 +374,66 @@ module Poetry
|
|
|
399
374
|
|
|
400
375
|
private
|
|
401
376
|
|
|
377
|
+
# One slot's registry definition: its name and arity, its doc, its
|
|
378
|
+
# component fact, its polymorphic types, then its setter facts.
|
|
379
|
+
def slot_definition(slot_name, config, context)
|
|
380
|
+
definition = { name: slot_name, many: config[:collection] == true }
|
|
381
|
+
doc = context[:slot_docs][slot_name.to_sym]
|
|
382
|
+
definition[:description] = doc if doc
|
|
383
|
+
renderable = config[:renderable]
|
|
384
|
+
definition[:component] = renderable.component_path if renderable.respond_to?(:component_path)
|
|
385
|
+
declared = declared_component(slot_name, config, context)
|
|
386
|
+
definition[:component] ||= declared if declared
|
|
387
|
+
definition[:types] = config[:renderable_hash].keys if config[:renderable_hash]
|
|
388
|
+
definition.merge!(setter_facts(slot_name, config, context))
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The component path a SLOT_RENDERS entry declares for a slot's
|
|
392
|
+
# first setter, or nil: a declared pure-forwarding lambda restores
|
|
393
|
+
# the component fact a wrapping lambda hides. Polymorphic slots
|
|
394
|
+
# stay out - their types are their contract.
|
|
395
|
+
def declared_component(slot_name, config, context)
|
|
396
|
+
return if config[:renderable_hash]
|
|
397
|
+
|
|
398
|
+
declared = context[:renders][slot_setters(slot_name, config).first&.to_sym]
|
|
399
|
+
return unless declared
|
|
400
|
+
unless declared.respond_to?(:component_path)
|
|
401
|
+
raise Poetry::Core::Error,
|
|
402
|
+
"#{context[:klass]}::SLOT_RENDERS[#{slot_name}] must be a poetry component class"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
declared.component_path
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# A slot's setter facts, each present only when it says something:
|
|
409
|
+
# positional arity, keyword names, the yieldless setters, the
|
|
410
|
+
# required content, and the builder surfaces.
|
|
411
|
+
def setter_facts(slot_name, config, context)
|
|
412
|
+
facts = {}
|
|
413
|
+
setter_args = setter_positional_args(slot_name, config)
|
|
414
|
+
facts[:setter_args] = setter_args unless setter_args.empty?
|
|
415
|
+
setter_kwargs = setter_keyword_args(slot_name, config)
|
|
416
|
+
facts[:setter_kwargs] = setter_kwargs unless setter_kwargs.empty?
|
|
417
|
+
yieldless = yieldless_setters(slot_name, config) - context[:block_yields].keys
|
|
418
|
+
facts[:yieldless] = yieldless unless yieldless.empty?
|
|
419
|
+
required = context[:required_content].slice(*slot_setters(slot_name, config).map(&:to_sym))
|
|
420
|
+
facts[:required_content] = required unless required.empty?
|
|
421
|
+
surfaces = builder_surfaces(slot_name, config, context[:builders], context[:seen])
|
|
422
|
+
facts[:builders] = surfaces unless surfaces.empty?
|
|
423
|
+
facts
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# The class's SLOT_BUILDERS map, or an empty one.
|
|
402
427
|
def declared_builders(klass)
|
|
403
428
|
declared_constant(klass, :SLOT_BUILDERS)
|
|
404
429
|
end
|
|
405
430
|
|
|
431
|
+
# The class's SLOT_REQUIRED_CONTENT map, or an empty one.
|
|
406
432
|
def declared_required_content(klass)
|
|
407
433
|
declared_constant(klass, :SLOT_REQUIRED_CONTENT)
|
|
408
434
|
end
|
|
409
435
|
|
|
436
|
+
# A constant on the class as a hash, or an empty hash when it is absent or unresolvable.
|
|
410
437
|
def declared_constant(klass, name)
|
|
411
438
|
klass.const_defined?(name) ? klass.const_get(name) : {}
|
|
412
439
|
rescue NameError
|
|
@@ -431,6 +458,7 @@ module Poetry
|
|
|
431
458
|
[config[:collection] ? name.delete_suffix("s") : name]
|
|
432
459
|
end
|
|
433
460
|
|
|
461
|
+
# Each setter's positional arity for a slot, per polymorphic type when it has them.
|
|
434
462
|
def setter_positional_args(slot_name, config)
|
|
435
463
|
if (types = config[:renderable_hash])
|
|
436
464
|
types.filter_map do |type, definition|
|
|
@@ -444,6 +472,7 @@ module Poetry
|
|
|
444
472
|
end
|
|
445
473
|
end
|
|
446
474
|
|
|
475
|
+
# Each setter's keyword names for a slot, per polymorphic type when it has them.
|
|
447
476
|
def setter_keyword_args(slot_name, config)
|
|
448
477
|
if (types = config[:renderable_hash])
|
|
449
478
|
types.filter_map do |type, definition|
|
|
@@ -457,6 +486,7 @@ module Poetry
|
|
|
457
486
|
end
|
|
458
487
|
end
|
|
459
488
|
|
|
489
|
+
# The slot's setters whose lambda consumes the block, so a caller's block param would be nil.
|
|
460
490
|
def yieldless_setters(slot_name, config)
|
|
461
491
|
if (types = config[:renderable_hash])
|
|
462
492
|
types.filter_map do |type, definition|
|
|
@@ -510,6 +540,7 @@ module Poetry
|
|
|
510
540
|
nil
|
|
511
541
|
end
|
|
512
542
|
|
|
543
|
+
# The slot surfaces of the builder classes a slot's setters yield, recursing once per builder.
|
|
513
544
|
def builder_surfaces(slot_name, config, builders, seen)
|
|
514
545
|
slot_setters(slot_name, config).filter_map do |setter|
|
|
515
546
|
builder = builders[setter.to_sym]
|
|
@@ -91,6 +91,7 @@ module Poetry
|
|
|
91
91
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
+
# Raises unless the part name is a kebab-case data-slot value.
|
|
94
95
|
def validate_name!(klass, name)
|
|
95
96
|
return if name.is_a?(String) && name.match?(PART_NAME)
|
|
96
97
|
|
|
@@ -98,6 +99,7 @@ module Poetry
|
|
|
98
99
|
"#{klass}: part name #{name.inspect} must be a kebab-case data-slot value"
|
|
99
100
|
end
|
|
100
101
|
|
|
102
|
+
# One declared state for a part: the data attribute, its condition and its values, validated.
|
|
101
103
|
def build_state(klass, part, attr, spec)
|
|
102
104
|
unless attr.is_a?(String) && attr.match?(STATE_ATTRIBUTE)
|
|
103
105
|
raise Poetry::Core::Error,
|
|
@@ -122,6 +124,7 @@ module Poetry
|
|
|
122
124
|
state
|
|
123
125
|
end
|
|
124
126
|
|
|
127
|
+
# A state spec's condition and values, from a bare condition or a hash.
|
|
125
128
|
def unpack_state(spec)
|
|
126
129
|
return [spec, nil] unless spec.is_a?(Hash)
|
|
127
130
|
|
|
@@ -129,6 +132,7 @@ module Poetry
|
|
|
129
132
|
[normalized["condition"], normalized["values"]]
|
|
130
133
|
end
|
|
131
134
|
|
|
135
|
+
# One declared custom property for a part, validated with its description.
|
|
132
136
|
def build_var(klass, part, var, description)
|
|
133
137
|
unless var.is_a?(String) && var.match?(VAR_NAME)
|
|
134
138
|
raise Poetry::Core::Error,
|
|
@@ -167,6 +167,7 @@ module Poetry
|
|
|
167
167
|
|
|
168
168
|
private
|
|
169
169
|
|
|
170
|
+
# The controller and name from a one- or two-argument descriptor call.
|
|
170
171
|
def unpack_stimulus_descriptor_args(args, kind)
|
|
171
172
|
case args.size
|
|
172
173
|
when 1 then [nil, args.first]
|
|
@@ -177,6 +178,7 @@ module Poetry
|
|
|
177
178
|
end
|
|
178
179
|
end
|
|
179
180
|
|
|
181
|
+
# The controller identifier a descriptor names, or the one declaration that owns the name.
|
|
180
182
|
def resolve_stimulus_descriptor(controller, name, kind:)
|
|
181
183
|
return resolve_stimulus_identifier(controller) if controller
|
|
182
184
|
|
|
@@ -197,6 +199,7 @@ module Poetry
|
|
|
197
199
|
"stimulus_#{kind}(:controller, #{name.inspect})"
|
|
198
200
|
end
|
|
199
201
|
|
|
202
|
+
# Whether a controller's manifest lists the name as an action method or an event.
|
|
200
203
|
def stimulus_descriptor_match?(identifier, name, kind)
|
|
201
204
|
definition = Poetry::Core::Stimulus::Manifest.definition(identifier)
|
|
202
205
|
return false unless definition
|
|
@@ -212,6 +215,7 @@ module Poetry
|
|
|
212
215
|
end
|
|
213
216
|
end
|
|
214
217
|
|
|
218
|
+
# Records an element's declaration, merging its wirings into an earlier declaration of the same name.
|
|
215
219
|
def store_stimulus_element(element)
|
|
216
220
|
existing = own_stimulus_elements[element.name]
|
|
217
221
|
own_stimulus_elements[element.name] =
|
|
@@ -183,7 +183,7 @@ module Poetry
|
|
|
183
183
|
# Extracts and processes style-specific options from the options hash.
|
|
184
184
|
#
|
|
185
185
|
# @param options [Hash] the original options hash
|
|
186
|
-
# @return [Array
|
|
186
|
+
# @return [Array(Object, Boolean, Object)] variants, required, and default_value
|
|
187
187
|
def extract_style_options(options)
|
|
188
188
|
variants = options.delete(:variants)
|
|
189
189
|
required, default_value = extract_declared_defaults(options)
|
|
@@ -2,13 +2,6 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
import { enterPresence, exitPresence, measurePresence } from "@poetry/controllers/helpers/presence"
|
|
3
3
|
import { setState, stateOf } from "@poetry/controllers/helpers/state"
|
|
4
4
|
|
|
5
|
-
// The accordion open-set machine: single (optionally collapsible)
|
|
6
|
-
// or multiple. Composes with poetry--core--roving-focus (manageTabindex:
|
|
7
|
-
// false - APG keeps every trigger tabbable) attached separately on the
|
|
8
|
-
// same root. Panels ride the presence helper; the measured
|
|
9
|
-
// --accordion-panel-height var feeds the vendored accordion-down/up
|
|
10
|
-
// keyframes.
|
|
11
|
-
|
|
12
5
|
// Safety net (ms) for clearing the data-transitioning window when
|
|
13
6
|
// animationend never arrives - reduced motion or a zero-length animation.
|
|
14
7
|
// Comfortably longer than the 0.2s accordion keyframe.
|
|
@@ -17,13 +10,23 @@ const TRANSITION_FALLBACK_MS = 400
|
|
|
17
10
|
// The component-facing event namespace (the poetry:<component> rule).
|
|
18
11
|
const EVENT_PREFIX = "poetry:accordion"
|
|
19
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The accordion open-set machine: single (optionally collapsible)
|
|
15
|
+
* or multiple. Composes with poetry--core--roving-focus (manageTabindex:
|
|
16
|
+
* false - APG keeps every trigger tabbable) attached separately on the
|
|
17
|
+
* same root. Panels ride the presence helper; the measured
|
|
18
|
+
* --accordion-panel-height var feeds the vendored accordion-down/up
|
|
19
|
+
* keyframes.
|
|
20
|
+
*/
|
|
20
21
|
export default class extends Controller {
|
|
21
22
|
// The events this controller dispatches (manifest surface;
|
|
22
23
|
// events_declaration.test.js enforces the list stays honest).
|
|
23
24
|
static events = ["poetry:accordion:change"]
|
|
24
25
|
|
|
25
26
|
static values = {
|
|
27
|
+
// single keeps one item open at a time; multiple lets any number stay open.
|
|
26
28
|
type: { type: String, default: "single" },
|
|
29
|
+
// In single mode, whether the open item can be closed by its own trigger.
|
|
27
30
|
collapsible: { type: Boolean, default: false }
|
|
28
31
|
}
|
|
29
32
|
|
|
@@ -2,14 +2,16 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
import { announce } from "@poetry/controllers/helpers/announce"
|
|
3
3
|
import { isImeKeydown } from "@poetry/controllers/helpers/escape"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The floating bulk-actions bar (the ActionBar contract): shows
|
|
7
|
+
* while its table's selection is non-empty, and holds the contract rules -
|
|
8
|
+
* focus NEVER moves in on show; if focus was inside when the bar hides,
|
|
9
|
+
* it returns to where it was before entering (the FocusScope restoreFocus
|
|
10
|
+
* equivalent, scoped small); "Actions available." is announced ONCE per
|
|
11
|
+
* appearance; the visible count RETAINS its last non-zero value while the
|
|
12
|
+
* bar animates out (never a "None selected" flash); Escape anywhere
|
|
13
|
+
* inside clears the selection (the table's engine listens for
|
|
14
|
+
*/
|
|
13
15
|
export default class ActionBarController extends Controller {
|
|
14
16
|
// The events this controller dispatches (manifest surface;
|
|
15
17
|
// events_declaration.test.js enforces the list stays honest).
|
|
@@ -17,6 +19,8 @@ export default class ActionBarController extends Controller {
|
|
|
17
19
|
|
|
18
20
|
static targets = ["count"]
|
|
19
21
|
static values = {
|
|
22
|
+
// The count announcement, with %{count} replaced by the number of selected
|
|
23
|
+
// rows.
|
|
20
24
|
label: { type: String, default: "%{count} selected" }
|
|
21
25
|
}
|
|
22
26
|
|