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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.controller_docs +1 -0
  3. data/.jsdoc_floor +1 -0
  4. data/.mcp.json +8 -0
  5. data/.yard-lint.yml +28 -0
  6. data/.yard_coverage_all +1 -0
  7. data/Archspec.rb +22 -0
  8. data/CHANGELOG.md +23 -0
  9. data/app/components/poetry/core/component.rb +98 -6
  10. data/app/components/poetry/core/concerns/agent_tools.rb +6 -0
  11. data/app/components/poetry/core/concerns/declared_attributes.rb +1 -1
  12. data/app/components/poetry/core/concerns/introspection.rb +67 -36
  13. data/app/components/poetry/core/concerns/parts.rb +4 -0
  14. data/app/components/poetry/core/concerns/stimulus.rb +4 -0
  15. data/app/components/poetry/core/concerns/styles.rb +1 -1
  16. data/app/javascript/poetry/core/accordion_controller.js +10 -7
  17. data/app/javascript/poetry/core/action_bar_controller.js +12 -8
  18. data/app/javascript/poetry/core/autocomplete_controller.js +15 -11
  19. data/app/javascript/poetry/core/calendar_controller.js +36 -29
  20. data/app/javascript/poetry/core/carousel_controller.js +13 -9
  21. data/app/javascript/poetry/core/checkbox_group_controller.js +19 -17
  22. data/app/javascript/poetry/core/checked_controller.js +32 -28
  23. data/app/javascript/poetry/core/clipboard_text_controller.js +10 -8
  24. data/app/javascript/poetry/core/combobox_controller.js +53 -47
  25. data/app/javascript/poetry/core/command_controller.js +35 -37
  26. data/app/javascript/poetry/core/context_menu_controller.js +28 -24
  27. data/app/javascript/poetry/core/date_field_controller.js +22 -16
  28. data/app/javascript/poetry/core/date_picker_controller.js +11 -6
  29. data/app/javascript/poetry/core/deferred_controller.js +26 -21
  30. data/app/javascript/poetry/core/dialog_controller.js +11 -9
  31. data/app/javascript/poetry/core/dismissable_controller.js +9 -5
  32. data/app/javascript/poetry/core/drawer_controller.js +24 -21
  33. data/app/javascript/poetry/core/file_input_controller.js +14 -11
  34. data/app/javascript/poetry/core/focus_scope_controller.js +11 -7
  35. data/app/javascript/poetry/core/hotkey_controller.js +15 -11
  36. data/app/javascript/poetry/core/hover_card_controller.js +25 -19
  37. data/app/javascript/poetry/core/mask_controller.js +42 -33
  38. data/app/javascript/poetry/core/menu_controller.js +40 -32
  39. data/app/javascript/poetry/core/menubar_controller.js +34 -30
  40. data/app/javascript/poetry/core/message_scroller_controller.js +29 -1
  41. data/app/javascript/poetry/core/navigation_menu_controller.js +27 -22
  42. data/app/javascript/poetry/core/number_field_controller.js +23 -11
  43. data/app/javascript/poetry/core/optimistic_form_controller.js +20 -18
  44. data/app/javascript/poetry/core/otp_controller.js +26 -22
  45. data/app/javascript/poetry/core/popover_controller.js +26 -22
  46. data/app/javascript/poetry/core/popper_controller.js +33 -27
  47. data/app/javascript/poetry/core/pressed_controller.js +15 -13
  48. data/app/javascript/poetry/core/questionnaire_controller.js +19 -13
  49. data/app/javascript/poetry/core/radio_group_controller.js +21 -18
  50. data/app/javascript/poetry/core/resizable_controller.js +11 -8
  51. data/app/javascript/poetry/core/roving_focus_controller.js +16 -13
  52. data/app/javascript/poetry/core/scroll_spy_controller.js +14 -12
  53. data/app/javascript/poetry/core/search_field_controller.js +10 -8
  54. data/app/javascript/poetry/core/select_controller.js +34 -24
  55. data/app/javascript/poetry/core/sensitive_input_controller.js +16 -14
  56. data/app/javascript/poetry/core/sheet_controller.js +10 -8
  57. data/app/javascript/poetry/core/sidebar_controller.js +23 -18
  58. data/app/javascript/poetry/core/slider_controller.js +37 -28
  59. data/app/javascript/poetry/core/state_controller.js +16 -11
  60. data/app/javascript/poetry/core/table_selection_controller.js +20 -16
  61. data/app/javascript/poetry/core/tabs_controller.js +17 -15
  62. data/app/javascript/poetry/core/tag_group_controller.js +21 -19
  63. data/app/javascript/poetry/core/toast_controller.js +20 -15
  64. data/app/javascript/poetry/core/toast_trigger_controller.js +7 -5
  65. data/app/javascript/poetry/core/toaster_controller.js +20 -15
  66. data/app/javascript/poetry/core/toggle_group_controller.js +21 -18
  67. data/app/javascript/poetry/core/tooltip_controller.js +23 -18
  68. data/app/javascript/poetry/core/tree_controller.js +14 -12
  69. data/config/component_registry.yml +33 -0
  70. data/config/controllers_manifest.json +792 -190
  71. data/eslint.config.mjs +35 -0
  72. data/lib/poetry/core/api_internals.rb +102 -0
  73. data/lib/poetry/core/check/stable_identity.rb +3 -0
  74. data/lib/poetry/core/check.rb +189 -53
  75. data/lib/poetry/core/config.rb +3 -11
  76. data/lib/poetry/core/contrib/wrapped_helper.rb +1 -2
  77. data/lib/poetry/core/css/bem_reference.rb +8 -0
  78. data/lib/poetry/core/css/modes.rb +4 -0
  79. data/lib/poetry/core/css/override_scan.rb +8 -0
  80. data/lib/poetry/core/css/resolver.rb +8 -0
  81. data/lib/poetry/core/css/safelist.rb +3 -0
  82. data/lib/poetry/core/css/tailwind_merger.rb +1 -2
  83. data/lib/poetry/core/css/template_classes.rb +6 -0
  84. data/lib/poetry/core/css/theme_coverage.rb +4 -0
  85. data/lib/poetry/core/css/token_collisions.rb +6 -0
  86. data/lib/poetry/core/css/var_coverage.rb +6 -0
  87. data/lib/poetry/core/css/verifier.rb +5 -0
  88. data/lib/poetry/core/design_lint.rb +57 -0
  89. data/lib/poetry/core/design_md/import.rb +16 -0
  90. data/lib/poetry/core/design_md.rb +29 -0
  91. data/lib/poetry/core/errors.rb +2 -0
  92. data/lib/poetry/core/host_components.rb +9 -0
  93. data/lib/poetry/core/host_helpers.rb +5 -0
  94. data/lib/poetry/core/html/attributes.rb +1 -1
  95. data/lib/poetry/core/icons.rb +3 -1
  96. data/lib/poetry/core/llms_text.rb +14 -11
  97. data/lib/poetry/core/page_architectures.rb +2 -0
  98. data/lib/poetry/core/part_contract.rb +21 -0
  99. data/lib/poetry/core/preview/abstract.rb +3 -2
  100. data/lib/poetry/core/preview/base.rb +3 -2
  101. data/lib/poetry/core/preview/sidecarable.rb +2 -2
  102. data/lib/poetry/core/preview/template.rb +4 -3
  103. data/lib/poetry/core/recipe_items.rb +5 -0
  104. data/lib/poetry/core/registry.rb +93 -50
  105. data/lib/poetry/core/registry_client.rb +15 -0
  106. data/lib/poetry/core/registry_installer.rb +14 -0
  107. data/lib/poetry/core/registry_items.rb +9 -0
  108. data/lib/poetry/core/requires_any.rb +25 -0
  109. data/lib/poetry/core/skill_text.rb +8 -0
  110. data/lib/poetry/core/stable_id.rb +3 -0
  111. data/lib/poetry/core/stimulus/builder.rb +2 -1
  112. data/lib/poetry/core/stimulus/declarations.rb +10 -5
  113. data/lib/poetry/core/stimulus/host_manifest.rb +19 -12
  114. data/lib/poetry/core/stimulus_contract.rb +16 -0
  115. data/lib/poetry/core/template_compile.rb +4 -0
  116. data/lib/poetry/core/token_import.rb +10 -0
  117. data/lib/poetry/core/tokens/color.rb +2 -0
  118. data/lib/poetry/core/tokens/contrast_gate.rb +6 -0
  119. data/lib/poetry/core/tokens/generator.rb +6 -0
  120. data/lib/poetry/core/version.rb +1 -1
  121. metadata +11 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74940c713a7f81772d930f9581313847ec1a4044f36c3b31774efafd31bd07ef
4
- data.tar.gz: 9e869c5574bef206ee03a998d31beda8cc10cf6f70e429134fb80b70fec8e2be
3
+ metadata.gz: af029ac85cadd2b3906f885dc1123ca58a73bcd55ffe07041abde3933a5a08f4
4
+ data.tar.gz: 3c5efbc74950c59708605ee7d77371d4c1b3f47e3fefc661042cfe9ce4179a34
5
5
  SHA512:
6
- metadata.gz: 6bfcc4296038b8766014f1ed52cda1e9c365a9242aabcc0f8bac7a864e63c64a01438ec58d46c48ed1525d0191f7d1e0e9b4826932f996cb0184de123a19f856
7
- data.tar.gz: c5c56a37ae235c3d88b155a9197138c9f583187814431d957f78efb430c8e925a86fc039111df305fec8d00b90e0b4cfe46eb897eca3415e8bc97b7d0182b670
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
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "rubydex": {
4
+ "command": "bundle",
5
+ "args": ["exec", "rdx", "mcp"]
6
+ }
7
+ }
8
+ }
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
@@ -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<(Boolean, Object)>] required and default_value
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
- builders = declared_builders(klass)
286
- required_content = declared_required_content(klass)
287
- block_yields = declared_constant(klass, :SLOT_BLOCK_YIELDS)
288
- renders = declared_constant(klass, :SLOT_RENDERS)
289
- slot_docs = klass.respond_to?(:slot_docs) ? klass.slot_docs : {}
290
- klass.registered_slots.map do |slot_name, config|
291
- definition = { name: slot_name, many: config[:collection] == true }
292
- doc = slot_docs[slot_name.to_sym]
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<(Object, Boolean, Object)>] variants, required, and default_value
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
- // The floating bulk-actions bar (the ActionBar contract): shows
6
- // while its table's selection is non-empty, and holds the contract rules -
7
- // focus NEVER moves in on show; if focus was inside when the bar hides,
8
- // it returns to where it was before entering (the FocusScope restoreFocus
9
- // equivalent, scoped small); "Actions available." is announced ONCE per
10
- // appearance; the visible count RETAINS its last non-zero value while the
11
- // bar animates out (never a "None selected" flash); Escape anywhere
12
- // inside clears the selection (the table's engine listens for
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