poetry-ui 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1346a1ff6a62e643e9440c7c42e835d643b55ea2dbcf8bc7938d800964995306
4
- data.tar.gz: a569d21afd89f4f611309bab9c05492bc47d1ff4e9997d1178bcfd8859c840de
3
+ metadata.gz: be9d330390d3bf48c36cd783dfaf4fdd00a0cf6073b9b8944caf10104abf66b7
4
+ data.tar.gz: 99aa8e106788b9a8d597b149e68c8d6353c7a49876a32969f106327a7d1be813
5
5
  SHA512:
6
- metadata.gz: '01872acf5407b8d3b14bb91c10f33f19b9fca4124f39338d8f88560c86bb6d41913ff50b92ef3dfa3293f4d14500fd3faa67395109a6d6f41a16412ab80c26ed'
7
- data.tar.gz: 4180cbf5784bc923d57da1394c91542959570209ae1f8beb89e68d31e1c80f7ec5c0809fc65f363f6c6b3b025e72851f0ec08614b2020e94bee5df44ca6ccea0
6
+ metadata.gz: 6f60d8c522645d60374997d18bcea9ff4baaeb2f5fd888374966d008ef362f216b7cb0fa2f022f4f6b2127c10aa6750de2e37546c9aec380b37e79a909cee29a
7
+ data.tar.gz: 1b8bf7807b8f0836d702577677038fd1d42a5b2a8283579eee0471bd20026de193487d42ebe5861615fa9b2b1969087e1201a7fb5791b6715c4d6cdec63824d9
data/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.1.1] - 2026-09-08
4
+
5
+ ### Added
6
+
7
+ - `content_class:` on `poetry_dropdown_menu` and `poetry_context_menu`, on Menubar's `with_menu`, and on every menu family's `with_sub`: the panel's class merge seam, the one Popover, HoverCard and Dialog already had. A dropdown panel opens at its trigger's width (minimum 8rem), so `content_class: "w-56"` is how a menu with icons, shortcuts or long labels gets its width.
8
+ - NavigationMenu `with_item(title, value:, disabled: true)`: an inert trigger (`disabled` + `data-disabled`) that hover, click and the arrows skip. `disabled:` on a link raises.
9
+ - `Poetry::Ui::Chat::Error`, a `Poetry::Core::Error`: `AssistantTurn#continuation_frames` raises it on a turn without an approval pause, where a missing constant raised NameError before.
10
+ - `poetry:install --skip-bundle`.
11
+
12
+ ### Changed
13
+
14
+ - `poetry:install` adds the `herb` gem to the development group (once, never when the Gemfile already declares it) and runs `bundle install`, so `bin/rails poetry:check` works on a fresh app with no manual step. `poetry:check`'s missing-parser message points at the installer.
15
+ - The upstream pin is shadcn@4.21.0. The theme CSS is unchanged between the releases; both fidelity ledgers are re-snapshotted at the new pin.
16
+ - MessageScroller renders `data-pending-scroll` on its root and viewport for `:end` and `:"last-anchor"`; the viewport hides while it is set (visibility, so the layout stays) and the hold lifts without JavaScript. The part contracts declare the state.
17
+ - Select, Combobox and Command group headings are `aria-hidden`: the group's name flows through `aria-labelledby`, so screen readers stop announcing the heading a second time. Menu groups wire `aria-labelledby` to a label rendered inside them and hide it the same way; a label outside any group stays plain text.
18
+ - HoverCard's trigger wiring is `pointerdown->pointerDown` (was `touchstart->touchGuard`), and NumberField's input no longer wires a `focus` action. A copy made with `poetry:add hover_card` or `poetry:add number_field` before this version carries the old wiring and logs a Stimulus "undefined method" error on those events: run `bin/rails g poetry:diff` and update the two lines. Apps on the gem-owned tier need nothing.
19
+ - ToastTrigger names its Button root through `identity:`.
20
+ - NavigationMenu's agent rules state the keyboard contract: ArrowLeft/ArrowRight between triggers and links, ArrowDown opens the focused trigger's panel, Escape closes.
21
+
22
+ ### Fixed
23
+
24
+ - HoverCard: a tap on the trigger clicks through on touch devices (the collapsed sidebar's icons did nothing on a tablet).
25
+ - NumberField: Tab's select-all and a click's caret placement stay native.
4
26
 
5
27
  ## [0.1.0] - 2026-09-05
6
28
 
@@ -236,9 +236,12 @@ module Poetry
236
236
  "#{group_id}-heading"
237
237
  end
238
238
 
239
+ # The group's name flows through aria-labelledby; aria-hidden keeps
240
+ # the text from being read a second time as a stray child of the
241
+ # listbox.
239
242
  def heading_part
240
243
  content_tag(:div, @heading_text, "data-slot" => "combobox-label", "id" => heading_id,
241
- "class" => Style.css(:label))
244
+ "aria-hidden" => "true", "class" => Style.css(:label))
242
245
  end
243
246
  end
244
247
 
@@ -207,10 +207,11 @@ module Poetry
207
207
  end
208
208
 
209
209
  # A styled heading, no ARIA role - the group points at it via
210
- # aria-labelledby.
210
+ # aria-labelledby, and aria-hidden keeps the text from being read a
211
+ # second time as a stray child of the listbox.
211
212
  def heading_part
212
213
  content_tag(:div, @heading_text, "data-slot" => "command-group-heading", "id" => heading_id,
213
- "class" => Style.css(:heading))
214
+ "aria-hidden" => "true", "class" => Style.css(:heading))
214
215
  end
215
216
  end
216
217
 
@@ -135,6 +135,9 @@ module Poetry
135
135
  option :loop, :boolean, default: false, doc: "Wraps arrow-key movement past either end of the menu."
136
136
  option :long_press_delay, :integer, default: 700, doc: "Touch long-press duration in ms before the menu opens."
137
137
  option :disabled, :boolean, default: false, doc: "Inerts the surface - no gesture opens the menu."
138
+ option :content_class, :string,
139
+ doc: "Class merge seam for the menu panel (e.g. content_class: \"w-48\"). Root-level " \
140
+ "class: styles the surface wrapper, not the panel."
138
141
  option :label, :string, doc: "The menu's accessible name (localized fallback when omitted)."
139
142
  option :focusable_surface, :boolean, default: false,
140
143
  doc: "Puts the surface in the tab order and advertises Shift+F10."
@@ -278,7 +281,7 @@ module Poetry
278
281
  "data-slot" => "context-menu-content", (open ? "data-open" : "data-closed") => "",
279
282
  # The initial placement (side: option; re-resolved live by popper).
280
283
  "data-side" => side.to_s, "data-align" => "start",
281
- "class" => css(:content)
284
+ "class" => css(:content, class: content_class)
282
285
  }.merge(stimulus_attributes_for(:content))
283
286
  attrs["hidden"] = true unless open
284
287
  attrs
@@ -142,6 +142,9 @@ module Poetry
142
142
  tool :open, description: "Open the drawer.", executes: :open, mutating: true
143
143
  tool :close, description: "Close the drawer.", executes: :close, mutating: true
144
144
 
145
+ # The inherited render checks, then the snap-point rules when
146
+ # snap_points: is given.
147
+ # @api private
145
148
  def before_render
146
149
  super
147
150
  validate_snap_points! if snap_points.present?
@@ -143,6 +143,10 @@ module Poetry
143
143
  option :loop, :boolean, default: false, doc: "Arrow-key navigation wraps from the last item back to the first."
144
144
  option :dir, :symbol, doc: "Reading direction; :rtl flips submenu sides and indicators."
145
145
  option :disabled, :boolean, default: false, doc: "Disables the menu trigger button."
146
+ option :content_class, :string,
147
+ doc: "Class merge seam for the menu panel - the panel opens at the trigger's width " \
148
+ "(min 8rem), so content_class: \"w-56\" widens it. Root-level class: styles the " \
149
+ "wrapper, not the panel."
146
150
 
147
151
  validates :side, inclusion: { in: SIDES }
148
152
  validates :align, inclusion: { in: ALIGNS }
@@ -272,7 +276,7 @@ module Poetry
272
276
  "data-slot" => "dropdown-menu-content", (open ? "data-open" : "data-closed") => "",
273
277
  # Initial placement, re-resolved live by popper on open.
274
278
  "data-side" => side, "data-align" => align,
275
- "class" => css(:content)
279
+ "class" => css(:content, class: content_class)
276
280
  }.merge(stimulus_attributes_for(:content))
277
281
  attrs["hidden"] = true unless open
278
282
  attrs
@@ -95,14 +95,15 @@ module Poetry
95
95
  end
96
96
  # The trigger handlers: pointerenter/leave pair
97
97
  # timers (touch excluded), focus opens immediately / blur closes,
98
- # and the touchstart guard (a tap navigates, never focus-opens).
98
+ # and the pointerdown latch (a tap or click never focus-opens; the
99
+ # tap keeps its click, so it navigates).
99
100
  on :trigger do
100
101
  controller :hover_card do
101
102
  action :pointer_enter, on: :pointerenter
102
103
  action :pointer_leave, on: :pointerleave
103
104
  action :focus_open, on: :focus
104
105
  action :blur_close, on: :blur
105
- action :touch_guard, on: :touchstart
106
+ action :pointer_down, on: :pointerdown
106
107
  end
107
108
  controller(:popper) { target :anchor }
108
109
  end
@@ -73,7 +73,7 @@ module Poetry
73
73
  renders_many :menus,
74
74
  doc: "The top-level menus. Each takes with_trigger (the menu button) plus the family item " \
75
75
  "slots (with_item, with_checkbox_item, with_radio_group, with_sub, with_separator, ...); " \
76
- "value: defaults to the menu's position.",
76
+ "value: defaults to the menu's position; content_class: is the panel's class merge seam.",
77
77
  renders: ->(**options) { Menu.new(bar: self, dir: dir, **options) }
78
78
 
79
79
  use_stimulus do
@@ -341,11 +341,12 @@ module Poetry
341
341
  content_tag(:button, Poetry::Core::HTML::Attributes.merged(attrs, options)) { capture(&block) }
342
342
  }
343
343
 
344
- def initialize(bar:, value: nil, disabled: false, dir: nil, **extra_attributes)
344
+ def initialize(bar:, value: nil, disabled: false, dir: nil, content_class: nil, **extra_attributes)
345
345
  super(extra_attributes)
346
346
  @bar = bar
347
347
  @disabled = disabled
348
348
  @dir = dir
349
+ @content_class = content_class
349
350
  position = @bar.register_menu(self)
350
351
  @value = (value || "menu-#{position}").to_s
351
352
  end
@@ -419,7 +420,7 @@ module Poetry
419
420
  "data-slot" => "menubar-content", (open? ? "data-open" : "data-closed") => "",
420
421
  # Initial placement, re-resolved live by popper on open.
421
422
  "data-side" => "bottom", "data-align" => "start",
422
- "class" => Style.css(:content)
423
+ "class" => Style.css(:content, class: @content_class)
423
424
  }.merge(stimulus_attributes(:popper) { |popper| popper.with_target(:content) })
424
425
  attrs["hidden"] = true unless open?
425
426
  content_tag(:div, attrs) { safe_join(items.map(&:to_s)) }
@@ -226,11 +226,28 @@ module Poetry
226
226
 
227
227
  def call
228
228
  attrs = { "data-slot" => "#{family_slot_prefix}-group", "role" => "group" }
229
+ attrs["aria-labelledby"] = label_id if @labelled
229
230
  content_tag(:div, Poetry::Core::HTML::Attributes.merged(attrs, html_attributes)) { safe_join(items.map(&:to_s)) }
230
231
  end
231
232
 
232
233
  private
233
234
 
235
+ # A label inside a group names the group (aria-labelledby) and is
236
+ # aria-hidden, so its text is not read a second time as a stray child
237
+ # of the menu; a label outside any group stays plain text.
238
+ def label_part(**, &)
239
+ @labelled = true
240
+ super("id" => label_id, "aria-hidden" => "true", **, &)
241
+ end
242
+
243
+ def group_id
244
+ @group_id ||= poetry_instance_id("poetry-#{family_slot_prefix}-group")
245
+ end
246
+
247
+ def label_id
248
+ "#{group_id}-label"
249
+ end
250
+
234
251
  def menu_dir
235
252
  @dir
236
253
  end
@@ -311,9 +328,11 @@ module Poetry
311
328
  end
312
329
  }
313
330
 
314
- def initialize(dir: nil, **extra_attributes)
331
+ # content_class: is the sub-panel's class merge seam (with_sub(content_class: "w-40")).
332
+ def initialize(dir: nil, content_class: nil, **extra_attributes)
315
333
  super(extra_attributes)
316
334
  @dir = dir
335
+ @content_class = content_class
317
336
  end
318
337
 
319
338
  def before_render
@@ -363,7 +382,7 @@ module Poetry
363
382
  "id" => content_id, "role" => "menu", "aria-orientation" => "vertical",
364
383
  "aria-labelledby" => trigger_id, "tabindex" => "-1",
365
384
  "data-slot" => "#{family_slot_prefix}-sub-content", "data-closed" => "", "hidden" => true,
366
- "class" => family_style.css(:sub_content)
385
+ "class" => family_style.css(:sub_content, class: @content_class)
367
386
  }.merge(stimulus_attributes(:popper) { |popper| popper.with_target(:content) })
368
387
  content_tag(:div, attrs) { safe_join(items.map(&:to_s)) }
369
388
  end
@@ -9,6 +9,16 @@ module Poetry
9
9
  # a jump-to-latest button. Without JavaScript it is a plain
10
10
  # scrollable region, fully readable.
11
11
  #
12
+ # Opening position: a scroll container always opens at the top, so a
13
+ # transcript opening at the newest message would paint the oldest
14
+ # rows first and jump once the controller runs. For :end and
15
+ # :"last-anchor" the root and the viewport render
16
+ # data-pending-scroll; the viewport's dictionary hides it while the
17
+ # attribute is present (visibility, so layout exists for the scroll)
18
+ # and the controller clears it once the position lands, or at once
19
+ # for an empty transcript. Without JavaScript the hold is lifted
20
+ # (a scripting: none media rule), so the region stays readable.
21
+ #
12
22
  # The content element is the Turbo Stream append target (stable
13
23
  # dom id "<id>-messages"); rows are poetry_message_scroller_item
14
24
  # wrappers keyed by message id. History prepends keep the reading
@@ -92,14 +102,20 @@ module Poetry
92
102
  "data-scrollable" => "overflow exists - carries which edges have room " \
93
103
  "(start, end, or both as a space-separated pair)",
94
104
  "data-autoscrolling" => "a programmatic scroll is settling - the " \
95
- "follow-bottom release is suppressed while set"
105
+ "follow-bottom release is suppressed while set",
106
+ "data-pending-scroll" => "the opening-position hold - rendered for " \
107
+ "default_scroll_position :end / :\"last-anchor\", " \
108
+ "cleared once the position lands (at once for an " \
109
+ "empty transcript)"
96
110
  }
97
111
  part "message-scroller-viewport", "The native scroll region (role=region, " \
98
112
  "focusable) - the controller mirrors the same " \
99
113
  "runtime attributes here",
100
114
  states: {
101
115
  "data-scrollable" => "overflow exists - the same edge tokens as the root",
102
- "data-autoscrolling" => "a programmatic scroll is settling"
116
+ "data-autoscrolling" => "a programmatic scroll is settling",
117
+ "data-pending-scroll" => "the opening-position hold (the root's) - the " \
118
+ "dictionary hides the viewport while it is set"
103
119
  }
104
120
  part "message-scroller-content", "The row container and Turbo Stream append target " \
105
121
  "(stable dom id <id>-messages); role=log announces " \
@@ -120,6 +136,7 @@ module Poetry
120
136
  { "data-slot" => "message-scroller" }
121
137
  .merge(stimulus_attributes_for(:root))
122
138
  .merge(component_data_attributes)
139
+ .merge(pending_scroll_attributes)
123
140
  )
124
141
  end
125
142
 
@@ -129,7 +146,13 @@ module Poetry
129
146
  "class" => css(:viewport), "data-slot" => "message-scroller-viewport",
130
147
  "role" => "region", "tabindex" => "0",
131
148
  "aria-label" => t("poetry.message_scroller.region")
132
- }.merge(stimulus_attributes_for(:viewport))
149
+ }.merge(stimulus_attributes_for(:viewport)).merge(pending_scroll_attributes)
150
+ end
151
+
152
+ # The opening-position hold (the class doc) - the server renders it so
153
+ # the first paint never shows the top of the thread.
154
+ def pending_scroll_attributes
155
+ %i[end last-anchor].include?(default_scroll_position) ? { "data-pending-scroll" => "" } : {}
133
156
  end
134
157
 
135
158
  # @api private
@@ -156,7 +179,8 @@ module Poetry
156
179
  }.merge(stimulus_attributes_for(:jump_button))
157
180
  end
158
181
 
159
- private :root_attributes, :viewport_attributes, :content_attributes, :spacer_attributes, :button_attributes
182
+ private :root_attributes, :viewport_attributes, :content_attributes, :spacer_attributes, :button_attributes,
183
+ :pending_scroll_attributes
160
184
  end
161
185
  end
162
186
  end
@@ -16,8 +16,14 @@ module Poetry
16
16
  # (rightly) rejects classes no build can produce.
17
17
  base "relative flex size-full min-h-0 flex-col overflow-hidden group/message-scroller"
18
18
 
19
+ # data-pending-scroll:invisible = the opening-position hold: the
20
+ # viewport hides (layout intact) until the controller lands the
21
+ # opening position. The noscript pair lifts it when scripts are off -
22
+ # the plain scrollable region stays readable, this component's no-JS
23
+ # contract.
19
24
  element :viewport, "size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable " \
20
- "overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-none"
25
+ "overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-none " \
26
+ "data-pending-scroll:invisible noscript:data-pending-scroll:visible"
21
27
 
22
28
  element :content, "cn-message-scroller-content flex h-max min-h-full flex-col"
23
29
 
@@ -30,27 +30,36 @@ module Poetry
30
30
  "Panel content is poetry_navigation_menu_link entries (active: marks the current page) - " \
31
31
  "never buttons; navigation navigates.",
32
32
  "This is a DISCLOSURE bar: Tab moves through it normally and nothing traps - do not " \
33
- "wire menu/menuitem roles.",
33
+ "wire menu/menuitem roles. Keys: ArrowLeft/ArrowRight step between triggers and links, " \
34
+ "ArrowDown opens the focused trigger's panel (focus stays on the trigger; Tab enters it), " \
35
+ "Escape closes and returns focus to the trigger.",
34
36
  "Rich panels (title + description grids) want viewport: true - the shared morphing " \
35
37
  "card contains and sizes them; the default per-item mode suits simple link lists " \
36
38
  "(the top-nav block shows the viewport pattern)."
37
39
  ].freeze
38
40
 
39
- # One declared bar entry - a link (href:) or a trigger + panel pair.
41
+ # One declared bar entry - a link (href:) or a trigger + panel pair
42
+ # (disabled: renders the trigger inert).
40
43
  # @api private
41
- Entry = Data.define(:title, :value, :href, :panel)
44
+ Entry = Data.define(:title, :value, :href, :panel, :disabled)
42
45
 
43
46
  renders_many :items,
44
- doc: "The bar entries. with_item(title, value:) { panel } declares a trigger + panel; " \
45
- "with_item(title, href:) a top-level link (with_link is the shorthand).",
46
- renders: lambda { |title, value: nil, href: nil, &panel|
47
+ doc: "The bar entries. with_item(title, value:) { panel } declares a trigger + panel " \
48
+ "(disabled: true renders the trigger inert - native disabled plus data-disabled; hover " \
49
+ "and click never open its panel); with_item(title, href:) a top-level link (with_link " \
50
+ "is the shorthand).",
51
+ renders: lambda { |title, value: nil, href: nil, disabled: false, &panel|
47
52
  if href.nil? && panel.nil?
48
53
  raise ArgumentError,
49
54
  "NavigationMenu item #{title.inspect} needs href: (a link) or a panel block"
50
55
  end
56
+ if href && disabled
57
+ raise ArgumentError,
58
+ "NavigationMenu item #{title.inspect}: disabled: applies to a trigger, not a link"
59
+ end
51
60
 
52
61
  entries << Entry.new(title: title, value: value&.to_s || title.to_s.parameterize, href: href,
53
- panel: panel)
62
+ panel: panel, disabled: disabled)
54
63
  nil
55
64
  }
56
65
 
@@ -116,7 +125,9 @@ module Poetry
116
125
  "data-popup-open" => "its panel is open (written with aria-expanded - the chevron " \
117
126
  "rotation hook)",
118
127
  "data-open" => "its panel is open (the controller writes both vocabularies)",
119
- "data-closed" => "its panel is closed (written after the first close)"
128
+ "data-closed" => "its panel is closed (written after the first close)",
129
+ "data-disabled" => "disabled: true - inert (native disabled rides along; hover and " \
130
+ "click never open the panel, the arrows step over it)"
120
131
  }
121
132
  part "navigation-menu-content", "One item's panel - presence-animated; in viewport mode it is " \
122
133
  "adopted into the shared viewport on first activation",
@@ -203,11 +214,16 @@ module Poetry
203
214
 
204
215
  # @api private
205
216
  def trigger_attributes(entry)
206
- {
217
+ attrs = {
207
218
  "type" => "button", "data-slot" => "navigation-menu-trigger",
208
219
  "aria-expanded" => "false", "aria-controls" => panel_id(entry),
209
220
  "class" => "#{css(:trigger)} group"
210
- }.merge(stimulus_attributes_for(:trigger))
221
+ }
222
+ if entry.disabled
223
+ attrs["disabled"] = true # inert: no focus, no click
224
+ attrs["data-disabled"] = "" # the contract's styling hook
225
+ end
226
+ attrs.merge(stimulus_attributes_for(:trigger))
211
227
  end
212
228
 
213
229
  # @api private
@@ -59,7 +59,6 @@ module Poetry
59
59
  target :input
60
60
  action :keydown, on: :keydown
61
61
  action :input, on: :input
62
- action :focus, on: :focus
63
62
  action :blur, on: :blur
64
63
  end
65
64
  end
@@ -626,12 +626,14 @@ module Poetry
626
626
  end
627
627
 
628
628
  # A styled heading, no ARIA role - the group points at it via
629
- # aria-labelledby.
629
+ # aria-labelledby, and aria-hidden keeps the text from being read a
630
+ # second time as a stray child of the listbox (the name still flows
631
+ # through the reference).
630
632
  def label_part
631
633
  return if @label_text.blank?
632
634
 
633
635
  content_tag(:div, @label_text, "data-slot" => "select-label", "id" => label_id,
634
- "class" => Style.css(:label))
636
+ "aria-hidden" => "true", "class" => Style.css(:label))
635
637
  end
636
638
  end
637
639
  end
@@ -65,12 +65,13 @@ module Poetry
65
65
  def button_options
66
66
  wiring = { data: { slot: "toast-trigger" } }
67
67
  .merge(stimulus_attributes_for(:root))
68
- .merge(component_data_attributes)
69
68
 
70
69
  # Caller attributes never clobber the trigger's wiring: both sides
71
70
  # flow through Attributes, so a host data-controller/action
72
- # concatenates instead of silently killing fire-on-click.
73
- { variant: variant, size: size }
71
+ # concatenates instead of silently killing fire-on-click. The
72
+ # Button root wears THIS component's identity through the
73
+ # identity: seam (the raw data-component is reserved).
74
+ { variant: variant, size: size, identity: self.class.component_title }
74
75
  .merge(Poetry::Core::HTML::Attributes.merged(wiring, html_attributes))
75
76
  .symbolize_keys
76
77
  end
@@ -2924,6 +2924,10 @@ components:
2924
2924
  bem_block: poetry-ui-context_menu
2925
2925
  styles: []
2926
2926
  options:
2927
+ - name: content_class
2928
+ type: string
2929
+ description: 'Class merge seam for the menu panel (e.g. content_class: "w-48").
2930
+ Root-level class: styles the surface wrapper, not the panel.'
2927
2931
  - name: dir
2928
2932
  type: symbol
2929
2933
  variants:
@@ -4527,6 +4531,11 @@ components:
4527
4531
  type: boolean
4528
4532
  default: true
4529
4533
  description: Flips/shifts placement to keep the menu inside the viewport.
4534
+ - name: content_class
4535
+ type: string
4536
+ description: 'Class merge seam for the menu panel - the panel opens at the trigger''s
4537
+ width (min 8rem), so content_class: "w-56" widens it. Root-level class: styles
4538
+ the wrapper, not the panel.'
4530
4539
  - name: dir
4531
4540
  type: symbol
4532
4541
  variants:
@@ -5651,9 +5660,9 @@ components:
5651
5660
  - blurClose
5652
5661
  - focusOpen
5653
5662
  - openValueChanged
5663
+ - pointerDown
5654
5664
  - pointerEnter
5655
5665
  - pointerLeave
5656
- - touchGuard
5657
5666
  events:
5658
5667
  - poetry:hover-card:closed
5659
5668
  - poetry:hover-card:open
@@ -5773,8 +5782,8 @@ components:
5773
5782
  'on': focus
5774
5783
  - method: blurClose
5775
5784
  'on': blur
5776
- - method: touchGuard
5777
- 'on': touchstart
5785
+ - method: pointerDown
5786
+ 'on': pointerdown
5778
5787
  - identifier: poetry--core--popper
5779
5788
  targets:
5780
5789
  - name: anchor
@@ -6362,7 +6371,8 @@ components:
6362
6371
  many: true
6363
6372
  description: 'The top-level menus. Each takes with_trigger (the menu button)
6364
6373
  plus the family item slots (with_item, with_checkbox_item, with_radio_group,
6365
- with_sub, with_separator, ...); value: defaults to the menu''s position.'
6374
+ with_sub, with_separator, ...); value: defaults to the menu''s position; content_class:
6375
+ is the panel''s class merge seam.'
6366
6376
  setter_args:
6367
6377
  menu: 0
6368
6378
  builders:
@@ -7083,7 +7093,7 @@ components:
7083
7093
  - content
7084
7094
  - item
7085
7095
  - button
7086
- capsule: cc8c5beaabc7
7096
+ capsule: 7baeb0a95efa
7087
7097
  controllers:
7088
7098
  - identifier: poetry--core--message-scroller
7089
7099
  targets:
@@ -7143,6 +7153,10 @@ components:
7143
7153
  - attr: data-autoscrolling
7144
7154
  condition: a programmatic scroll is settling - the follow-bottom release is
7145
7155
  suppressed while set
7156
+ - attr: data-pending-scroll
7157
+ condition: the opening-position hold - rendered for default_scroll_position
7158
+ :end / :"last-anchor", cleared once the position lands (at once for an empty
7159
+ transcript)
7146
7160
  - name: message-scroller-viewport
7147
7161
  description: The native scroll region (role=region, focusable) - the controller
7148
7162
  mirrors the same runtime attributes here
@@ -7151,6 +7165,9 @@ components:
7151
7165
  condition: overflow exists - the same edge tokens as the root
7152
7166
  - attr: data-autoscrolling
7153
7167
  condition: a programmatic scroll is settling
7168
+ - attr: data-pending-scroll
7169
+ condition: the opening-position hold (the root's) - the dictionary hides the
7170
+ viewport while it is set
7154
7171
  - name: message-scroller-content
7155
7172
  description: The row container and Turbo Stream append target (stable dom id
7156
7173
  <id>-messages); role=log announces additions
@@ -7420,9 +7437,10 @@ components:
7420
7437
  slots:
7421
7438
  - name: items
7422
7439
  many: true
7423
- description: The bar entries. with_item(title, value:) { panel } declares a
7424
- trigger + panel; with_item(title, href:) a top-level link (with_link is the
7425
- shorthand).
7440
+ description: 'The bar entries. with_item(title, value:) { panel } declares a
7441
+ trigger + panel (disabled: true renders the trigger inert - native disabled
7442
+ plus data-disabled; hover and click never open its panel); with_item(title,
7443
+ href:) a top-level link (with_link is the shorthand).'
7426
7444
  setter_args:
7427
7445
  item: 1
7428
7446
  yieldless:
@@ -7485,7 +7503,9 @@ components:
7485
7503
  - 'Panel content is poetry_navigation_menu_link entries (active: marks the current
7486
7504
  page) - never buttons; navigation navigates.'
7487
7505
  - 'This is a DISCLOSURE bar: Tab moves through it normally and nothing traps -
7488
- do not wire menu/menuitem roles.'
7506
+ do not wire menu/menuitem roles. Keys: ArrowLeft/ArrowRight step between triggers
7507
+ and links, ArrowDown opens the focused trigger''s panel (focus stays on the
7508
+ trigger; Tab enters it), Escape closes and returns focus to the trigger.'
7489
7509
  - 'Rich panels (title + description grids) want viewport: true - the shared morphing
7490
7510
  card contains and sizes them; the default per-item mode suits simple link lists
7491
7511
  (the top-nav block shows the viewport pattern).'
@@ -7513,6 +7533,9 @@ components:
7513
7533
  condition: its panel is open (the controller writes both vocabularies)
7514
7534
  - attr: data-closed
7515
7535
  condition: its panel is closed (written after the first close)
7536
+ - attr: data-disabled
7537
+ condition: 'disabled: true - inert (native disabled rides along; hover and
7538
+ click never open the panel, the arrows step over it)'
7516
7539
  - name: navigation-menu-content
7517
7540
  description: One item's panel - presence-animated; in viewport mode it is adopted
7518
7541
  into the shared viewport on first activation
@@ -7709,7 +7732,6 @@ components:
7709
7732
  - wheel
7710
7733
  actions:
7711
7734
  - blur
7712
- - focus
7713
7735
  - hiddenChanged
7714
7736
  - input
7715
7737
  - keydown
@@ -7788,8 +7810,6 @@ components:
7788
7810
  'on': keydown
7789
7811
  - method: input
7790
7812
  'on': input
7791
- - method: focus
7792
- 'on': focus
7793
7813
  - method: blur
7794
7814
  'on': blur
7795
7815
  - element: hidden
@@ -60,7 +60,7 @@ module Poetry
60
60
  the last edit (unknown components/slots/variants/wiring, icon names,
61
61
  enum values, typed-slot props, helper + setter arity, yield-less
62
62
  blocks, setter keywords, required content blocks, required slots,
63
- did-you-mean, `--json`; needs the `herb` gem in the Gemfile). An edit
63
+ did-you-mean, `--json`; `poetry:install` adds the `herb` gem it parses with). An edit
64
64
  made after your last check is unverified markup - re-run it.
65
65
  - Faster: the `poetry` MCP server (`.mcp.json`: command `bundle`, args
66
66
  `["exec", "poetry-agent"]`, the poetry-agent gem) serves ten tools from the live registry
@@ -43,6 +43,12 @@ module Poetry
43
43
 
44
44
  class_option :charts, type: :boolean, default: false,
45
45
  desc: %(Also wire poetry-charts (requires gem "poetry-charts" in the bundle))
46
+ class_option :skip_bundle, type: :boolean, default: false,
47
+ desc: "Skip `bundle install` after adding the herb gem (poetry:check's parser)"
48
+
49
+ # A Gemfile line declaring herb, in any quoting, at any indentation
50
+ # (a `group :development do` block included).
51
+ HERB_GEM_LINE = /^\s*gem\s+["']herb["']/
46
52
 
47
53
  # Install-time theme selection. Every themes/<name>.css fragment is
48
54
  # a complete visual theme; the chosen one fills the style-default.css
@@ -253,6 +259,29 @@ module Poetry
253
259
  JS
254
260
  end
255
261
 
262
+ # poetry:check parses ERB with herb, which stays out of poetry's runtime
263
+ # dependencies on purpose (hosts never need it to render), so the
264
+ # install adds it to the development group - once, and never when the
265
+ # Gemfile already declares it anywhere. Then `bundle install`, unless
266
+ # --skip-bundle (the note says what is left to do).
267
+ # @api private
268
+ def add_herb_gem
269
+ gemfile = File.join(destination_root, "Gemfile")
270
+ return unless File.exist?(gemfile)
271
+
272
+ if File.read(gemfile).match?(HERB_GEM_LINE)
273
+ say_status :skip, "herb is already in the Gemfile (poetry:check's parser)", :cyan
274
+ return
275
+ end
276
+
277
+ gem "herb", group: :development, comment: "poetry:check parses ERB with herb (development only)"
278
+ if options[:skip_bundle]
279
+ say_status :note, "run `bundle install` to fetch herb before `bin/rails poetry:check`", :cyan
280
+ else
281
+ in_root { Bundler.with_unbundled_env { run "bundle install" } }
282
+ end
283
+ end
284
+
256
285
  # The llms.txt / llms-full.txt agent docs are engine routes - without
257
286
  # the mount they are unreachable (the fresh-app proof caught this).
258
287
  # @api private
@@ -37,6 +37,11 @@ module Poetry
37
37
  Script.new(&)
38
38
  end
39
39
 
40
+ # Raised when a script is asked for what its shape cannot give -
41
+ # continuation frames from a turn that has no approval pause.
42
+ # Inherits the family base, so `rescue Poetry::Core::Error` covers it.
43
+ class Error < Poetry::Core::Error; end
44
+
40
45
  # One rendered state of an assistant row mid-stream.
41
46
  Frame = Struct.new(:parts, :sleep_ms, :version, keyword_init: true)
42
47
 
@@ -22,7 +22,7 @@ module Poetry
22
22
 
23
23
  # The provenance line template for ported treatments,
24
24
  # format-interpolated with the upstream style name.
25
- PORTED = "ported from the source style-%s.css at pin shadcn@4.19.0 (1773ecfe)"
25
+ PORTED = "ported from the source style-%s.css at pin shadcn@4.21.0 (7c9eaba1)"
26
26
 
27
27
  # Per-theme DESIGN.md metadata: the typography pairing and the
28
28
  # treatment provenance line.
@@ -3,6 +3,6 @@
3
3
  module Poetry
4
4
  module Ui
5
5
  # The gem version.
6
- VERSION = "0.1.0"
6
+ VERSION = "0.1.1"
7
7
  end
8
8
  end
@@ -12,7 +12,8 @@ namespace :poetry do
12
12
  # optional-parser posture as poetry:verify's template gate.
13
13
  unless Poetry::Core::CSS::TemplateClasses.available?
14
14
  abort "poetry:check: the herb gem is required to parse templates - " \
15
- "add `gem \"herb\"` to your Gemfile (development group is enough)"
15
+ "`bin/rails g poetry:install` adds it to your development group " \
16
+ "(or add `gem \"herb\"` to the Gemfile yourself and bundle)"
16
17
  end
17
18
 
18
19
  # Default sweep: ERB through the template tier, app Ruby through the
data/themes/luma.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* poetry luma theme - upstream style-luma.css ported onto the
2
- * cn-* layer (pinned clone 1773ecfe, the shadcn@4.19.0 release). Same contract as default.css:
2
+ * cn-* layer (pinned clone 7c9eaba1, the shadcn@4.21.0 release). Same contract as default.css:
3
3
  * imported layer(base); bare selectors while installs carry ONE theme;
4
4
  * rule order per component = base < elements < variants < compounds;
5
5
  * split-side, no-empty-rules and cross-component-last rules apply.
data/themes/lyra.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* poetry lyra theme - upstream style-lyra.css ported onto the
2
- * cn-* layer (pinned clone 1773ecfe, the shadcn@4.19.0 release). Same contract as default.css:
2
+ * cn-* layer (pinned clone 7c9eaba1, the shadcn@4.21.0 release). Same contract as default.css:
3
3
  * imported layer(base); bare selectors while installs carry ONE theme;
4
4
  * rule order per component = base < elements < variants < compounds;
5
5
  * split-side, no-empty-rules and cross-component-last rules apply.
data/themes/maia.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* poetry maia theme - upstream style-maia.css ported onto the
2
- * cn-* layer (pinned clone 1773ecfe, the shadcn@4.19.0 release). Same contract as default.css:
2
+ * cn-* layer (pinned clone 7c9eaba1, the shadcn@4.21.0 release). Same contract as default.css:
3
3
  * imported layer(base); bare selectors while installs carry ONE theme;
4
4
  * rule order per component = base < elements < variants < compounds;
5
5
  * split-side, no-empty-rules and cross-component-last rules apply.
data/themes/mira.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* poetry mira theme - upstream style-mira.css ported onto the
2
- * cn-* layer (pinned clone 1773ecfe, the shadcn@4.19.0 release). Same contract as default.css:
2
+ * cn-* layer (pinned clone 7c9eaba1, the shadcn@4.21.0 release). Same contract as default.css:
3
3
  * imported layer(base); bare selectors while installs carry ONE theme;
4
4
  * rule order per component = base < elements < variants < compounds;
5
5
  * split-side, no-empty-rules and cross-component-last rules apply.
data/themes/nova.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* poetry nova theme - upstream style-nova.css ported onto the
2
- * cn-* layer (pinned clone 1773ecfe, the shadcn@4.19.0 release). Same contract as default.css:
2
+ * cn-* layer (pinned clone 7c9eaba1, the shadcn@4.21.0 release). Same contract as default.css:
3
3
  * imported layer(base); bare selectors while installs carry ONE theme;
4
4
  * rule order per component = base < elements < variants < compounds;
5
5
  * split-side, no-empty-rules and cross-component-last rules apply.
data/themes/rhea.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* poetry rhea theme - upstream style-rhea.css ported onto the
2
- * cn-* layer (pinned clone 1773ecfe, the shadcn@4.19.0 release). Same contract as default.css:
2
+ * cn-* layer (pinned clone 7c9eaba1, the shadcn@4.21.0 release). Same contract as default.css:
3
3
  * imported layer(base); bare selectors while installs carry ONE theme;
4
4
  * rule order per component = base < elements < variants < compounds;
5
5
  * split-side, no-empty-rules and cross-component-last rules apply.
data/themes/sera.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* poetry sera theme - upstream style-sera.css ported onto the
2
- * cn-* layer (pinned clone 1773ecfe, the shadcn@4.19.0 release). Same contract as default.css:
2
+ * cn-* layer (pinned clone 7c9eaba1, the shadcn@4.21.0 release). Same contract as default.css:
3
3
  * imported layer(base); bare selectors while installs carry ONE theme;
4
4
  * rule order per component = base < elements < variants < compounds;
5
5
  * split-side, no-empty-rules and cross-component-last rules apply.
data/themes/vega.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* poetry vega theme - upstream style-vega.css ported onto the
2
- * cn-* layer (pinned clone 1773ecfe, the shadcn@4.19.0 release). Same contract as default.css:
2
+ * cn-* layer (pinned clone 7c9eaba1, the shadcn@4.21.0 release). Same contract as default.css:
3
3
  * imported layer(base); bare selectors while installs carry ONE theme
4
4
  * (the .style-<name> wrapper arrives with the docs switcher); rule order
5
5
  * per component = base < elements < variants < compounds; split-side,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: poetry-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Solt
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.0
18
+ version: 0.1.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.1.0
25
+ version: 0.1.1
26
26
  description: 'Poetry''s components for the AI-native UI component library: accessible,
27
27
  themeable, agent-legible ViewComponents built entirely on poetry-core''s public
28
28
  DSL.'