poetry-ui 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/app/components/poetry/ui/accordion/component.html.erb +1 -1
- data/app/components/poetry/ui/alert/component.html.erb +1 -1
- data/app/components/poetry/ui/alert_dialog/component.html.erb +2 -2
- data/app/components/poetry/ui/alert_dialog/component.rb +14 -2
- data/app/components/poetry/ui/attachment/component.html.erb +1 -1
- data/app/components/poetry/ui/autocomplete/component.html.erb +4 -4
- data/app/components/poetry/ui/autocomplete/component.rb +35 -14
- data/app/components/poetry/ui/avatar/component.rb +12 -16
- data/app/components/poetry/ui/breadcrumb/component.html.erb +1 -1
- data/app/components/poetry/ui/bubble/component.html.erb +2 -2
- data/app/components/poetry/ui/button/component.html.erb +1 -1
- data/app/components/poetry/ui/button/component.rb +9 -5
- data/app/components/poetry/ui/calendar/component.html.erb +9 -9
- data/app/components/poetry/ui/card/component.html.erb +4 -4
- data/app/components/poetry/ui/card/component.rb +2 -1
- data/app/components/poetry/ui/carousel/component.html.erb +3 -3
- data/app/components/poetry/ui/checkbox/component.html.erb +5 -5
- data/app/components/poetry/ui/clipboard_text/component.html.erb +4 -4
- data/app/components/poetry/ui/code_block/component.html.erb +2 -2
- data/app/components/poetry/ui/collapsible/component.html.erb +2 -2
- data/app/components/poetry/ui/collapsible/component.rb +16 -9
- data/app/components/poetry/ui/collapsible/preview.rb +2 -2
- data/app/components/poetry/ui/combobox/component.html.erb +6 -6
- data/app/components/poetry/ui/command/component.html.erb +3 -3
- data/app/components/poetry/ui/command/dialog_component.html.erb +2 -2
- data/app/components/poetry/ui/context_menu/component.html.erb +2 -2
- data/app/components/poetry/ui/data_table/component.html.erb +5 -5
- data/app/components/poetry/ui/date_field/component.html.erb +3 -3
- data/app/components/poetry/ui/date_picker/component.html.erb +2 -2
- data/app/components/poetry/ui/deferred/component.html.erb +3 -3
- data/app/components/poetry/ui/dialog/component.html.erb +2 -2
- data/app/components/poetry/ui/drawer/component.html.erb +2 -2
- data/app/components/poetry/ui/dropdown_menu/component.html.erb +2 -2
- data/app/components/poetry/ui/empty/component.html.erb +2 -2
- data/app/components/poetry/ui/field/component.html.erb +1 -1
- data/app/components/poetry/ui/field/component.rb +1 -1
- data/app/components/poetry/ui/field_separator/component.rb +9 -7
- data/app/components/poetry/ui/file_input/component.html.erb +4 -4
- data/app/components/poetry/ui/hover_card/component.html.erb +2 -2
- data/app/components/poetry/ui/input_otp/component.html.erb +3 -3
- data/app/components/poetry/ui/input_otp/component.rb +12 -4
- data/app/components/poetry/ui/item/component.html.erb +1 -1
- data/app/components/poetry/ui/marker/component.html.erb +1 -1
- data/app/components/poetry/ui/menubar/component.html.erb +1 -1
- data/app/components/poetry/ui/message/component.html.erb +1 -1
- data/app/components/poetry/ui/message_scroller/component.html.erb +1 -1
- data/app/components/poetry/ui/message_scroller/component.rb +14 -8
- data/app/components/poetry/ui/native_select/component.rb +15 -6
- data/app/components/poetry/ui/navigation_menu/bar_preview.html.erb +1 -1
- data/app/components/poetry/ui/navigation_menu/component.html.erb +6 -6
- data/app/components/poetry/ui/navigation_menu/component.rb +26 -11
- data/app/components/poetry/ui/number_field/component.html.erb +6 -6
- data/app/components/poetry/ui/number_field/component.rb +17 -4
- data/app/components/poetry/ui/pagination/component.html.erb +1 -1
- data/app/components/poetry/ui/popover/component.html.erb +2 -2
- data/app/components/poetry/ui/questionnaire/component.html.erb +5 -5
- data/app/components/poetry/ui/radio_group/component.html.erb +1 -1
- data/app/components/poetry/ui/resizable/component.html.erb +3 -3
- data/app/components/poetry/ui/search_field/component.html.erb +5 -5
- data/app/components/poetry/ui/select/component.html.erb +3 -3
- data/app/components/poetry/ui/sensitive_input/component.html.erb +6 -6
- data/app/components/poetry/ui/sheet/component.html.erb +2 -2
- data/app/components/poetry/ui/sidebar/component.html.erb +5 -5
- data/app/components/poetry/ui/slider/component.html.erb +5 -5
- data/app/components/poetry/ui/stat/component.html.erb +1 -1
- data/app/components/poetry/ui/switch/component.html.erb +5 -5
- data/app/components/poetry/ui/table/component.html.erb +2 -2
- data/app/components/poetry/ui/tabs/component.html.erb +4 -4
- data/app/components/poetry/ui/tag_group/component.html.erb +3 -3
- data/app/components/poetry/ui/toast/component.html.erb +1 -1
- data/app/components/poetry/ui/toaster/component.html.erb +1 -1
- data/app/components/poetry/ui/toggle_group/component.html.erb +1 -1
- data/app/components/poetry/ui/tooltip/component.html.erb +3 -3
- data/app/components/poetry/ui/tree/component.html.erb +3 -3
- data/app/helpers/poetry/ui/components_helper.rb +28 -7
- data/app/helpers/poetry/ui/form_builder/type_inference.rb +17 -1
- data/app/helpers/poetry/ui/form_builder.rb +202 -80
- data/config/component_registry.yml +90 -46
- data/config/template_classes.txt +3 -0
- data/lib/generators/poetry/agents_section.rb +3 -1
- data/lib/generators/poetry/block/templates/destructive_panel.html.erb +13 -3
- data/lib/generators/poetry/block/templates/top_nav.html.erb +2 -1
- data/lib/generators/poetry/editor/editor_generator.rb +8 -12
- data/lib/generators/poetry/install/install_generator.rb +19 -3
- data/lib/generators/poetry/skill/templates/poetry-component/references/checklist.md +6 -0
- data/lib/poetry/ui/testing/checkbox.rb +114 -0
- data/lib/poetry/ui/testing.rb +12 -0
- data/lib/poetry/ui/version.rb +1 -1
- data/lib/tasks/poetry/check.rake +21 -2
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 311078c0215e2e504b32301efa8e51f0e7e108d8788587337689f356e72423e0
|
|
4
|
+
data.tar.gz: 1d58b4704974eeb18b59a1f7a32f8d51fcb0598f846ba1d5864507b482d875eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f8a5636a3b349a718912cc30df3675b45858c9fcb0f15c50772e578f6c7c35fefa5729717207ecc0daa05dbf5aac44688896369001ac19b93e6430e5e2fab12
|
|
7
|
+
data.tar.gz: 6d587c9fa1a023a7ced7a1bf176c6b775198e0acb684781ae868321175ad91199f644e3f12081cf9624dc8dc19a1cd3489daf06f2933b5fdcb24b57b40714288
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.9] - 2026-09-30
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `bin/rails poetry:check` compiles the templates that sit beside components, which `bin/rails herb:check` never reaches: the app's own under `app/components/`, and the ones the gems in the boot ship. A template Herb refuses is a `herb-compile` finding, an error when the app renders through Herb (Rails' 8.2 defaults, or ReActionView) and a warning until then. A finding on a template a gem ships names what fixes it: the gem, or the herb the bundle resolves. A herb older than 0.11.0 leaves the compile out and says so on stderr, and the rest of the check runs as before.
|
|
8
|
+
- `rails g poetry:install` adds no `herb` line when Action View depends on the gem, as it does from Rails 8.2, and says where the parser comes from. On an earlier Rails it adds the line as before, and a `herb` the Gemfile declares is left alone on either.
|
|
9
|
+
- `rails g poetry:editor` writes a `.herb.yml` pinned to Herb 0.11.0. The two rules it used to switch off for slot setters (`erb-no-unused-expressions`, `actionview-no-silent-helper`) are fixed in 0.11 and stay on. One rule starts off instead: `html-no-nested-forms` reports a form inside a block helper, a `form_with` inside `poetry_card do`, as a form nested in another form. An existing `.herb.yml` is kept as it is; move its `version` to 0.11.0 and drop the two entries to get the same.
|
|
10
|
+
- Every component template builds its elements through `element_tag` and hands `tag.attributes` its hash as an argument, never as a splat. A component now renders the same in a host that compiles its templates with Herb's slots (ReActionView's `config.slots`) as in one that does not; before, such a host rendered most components without their root attributes and could not compile Button, Marker or Item at all. What a component renders without slots is unchanged, byte for byte: the suite, the behaviour tier and the 455 visual baselines hold.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- NumberField with `format:` renders on Rails 8.2, and through the renderer on any Rails. Rails reads `format` off what it renders and takes the answer for the format of the template. The option's reader answered with its Hash, so the render raised `Invalid formats`. A render from a controller asked already; from Rails 8.2 a render in a view asks too, which is every `poetry_number_field(format: ...)`. The option keeps its name and the component leaves Rails with its default.
|
|
15
|
+
|
|
16
|
+
## [0.1.8] - 2026-09-22
|
|
17
|
+
|
|
18
|
+
The robosite proof: a full Jumpstart Pro application re-skinned on Poetry surfaced 28 findings; the 20 that were Poetry's to fix land here, with the form builder taking most of them.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `form.checkbox_group` maps records through `value_method:` and `label_method:` (a symbol sent to the item, or a callable), and `checked:` names the checked values explicitly for a collection the object has no reader for.
|
|
23
|
+
- `form.file_input` flips the form to multipart (Rails' own `file_field` line), and a block given to it renders beside the control inside the Field: the seat for the current attachment's preview.
|
|
24
|
+
- Every builder method takes `label:` for the visible Field label (`label: false` drops it) and `aria_label:` for an accessible name alone; `label:` on a control used to become that control's `aria-label`, invisible.
|
|
25
|
+
- Model-less forms: `form_with(url:, scope:)` renders every builder method, with labels from the scope's i18n or the humanized method and empty values, where the object-less call raised before.
|
|
26
|
+
- NavigationMenu `with_link(title, href:, active: true)` marks the current page's top-level link (`data-active` plus `aria-current="page"`, the trigger's open treatment); the top-nav block marks its Docs link.
|
|
27
|
+
- AlertDialog `with_action(submit: "form-id")` makes the action that form's submit button (`type="submit"` plus the `form` attribute), so a confirmation submits a form on the page without nesting one inside the dialog; the destructive-panel block adopts it.
|
|
28
|
+
- MessageScroller `content_id:` names the content element's dom id directly (the default stays `"<id>-messages"`), and `poetry_message_scroller_item` takes `dom_id:` for a row's own DOM id beside its message id.
|
|
29
|
+
- NativeSelect routes `id:` and every `aria-*` attribute (flat, or nested `aria:`) onto the `<select>` itself, so a Field's `control_attributes` splat straight in; `described_by:` stays as the older spelling. The builder's `native_select` now hands the select `aria-required` too.
|
|
30
|
+
- Autocomplete lands the given `id:` on its `<input>` (the Field label's `for=` target) and derives the root, list and item ids from it; `aria-*` attributes move to the input the same way.
|
|
31
|
+
- The table part helpers consume `key:` (it never renders as an attribute): `poetry_table_row(key: record)` derives `id="table-row-<dom_id>"` for a morph to follow; the other parts drop it. The registry declares the seven parts `identity: false`, so `poetry check` stops asking them for a key.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `form.submit` renders the Button named `commit` with the label as its value, so `params[:commit]` reads as it does under Rails' own submit; `form.button` is named `button`, Rails' own name for it. Pass `name:` or `value:` to override either.
|
|
36
|
+
- `form.check_box` and `form.switch` render inside the horizontal Field with the visible label, the shape boolean `f.input` already had; the Rails arity (`checked_value`, `unchecked_value`) is unchanged.
|
|
37
|
+
- The builder reads `false` and `0` as values: a tristate select shows No and a zero count shows 0, where `.presence` had blanked both. A virtual attribute with no reader renders empty instead of raising.
|
|
38
|
+
- Labels resolve from Rails' `helpers.label.<object_name>.<method>` first, the key `f.label` reads, then `poetry_form.labels` / `simple_form.labels`, then `human_attribute_name`; placeholders read `helpers.placeholder.*` first. Hints and placeholders now resolve on direct control calls (`form.field`, `form.native_select`), not only through `f.input`.
|
|
39
|
+
- Collapsible's `with_trigger` composes a poetry Button, the way Popover's does: keywords are Button props (`variant:`, `size:`), and the trigger wears Button's treatment. The previews use `variant: :ghost, size: :sm`.
|
|
40
|
+
- Button's anchor form (`tag: :a`, or `href:`) renders no `role="button"`: a link stays a link, announced by its destination and activated by Enter. A caller's `role:` still lands.
|
|
41
|
+
- Avatar's `label:` is no longer required: without one the avatar renders decorative (`aria-hidden`, no `role="img"`) with its initials fallback, the right shape beside a visible name; the content block is still required.
|
|
42
|
+
- InputOTP stamps its default `aria-label` only when nothing names the input already: an `aria-label`, an `aria-labelledby`, or an `id` a Field label points at.
|
|
43
|
+
- Card renders its `card-content` cell only when the content block gives content; a header-and-footer card no longer carries an empty body cell.
|
|
44
|
+
- FieldSeparator is the labelled rule anywhere ("Or continue with" under a sign-in form, a date break), not only between fields in a FieldGroup.
|
|
45
|
+
|
|
3
46
|
## [0.1.7] - 2026-09-20
|
|
4
47
|
|
|
5
48
|
### Changed
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%=
|
|
1
|
+
<%= element_tag(:div, **root_attributes) do %>
|
|
2
2
|
<%= icon %>
|
|
3
3
|
<% if title? %><div data-slot="alert-title" class="<%= css(:title) %>"><%= title %></div><% end %>
|
|
4
4
|
<div data-slot="alert-description" class="<%= css(:description) %>"><%= content %></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<%=
|
|
1
|
+
<%= element_tag(:div, **root_attributes) do %>
|
|
2
2
|
<%= trigger %>
|
|
3
|
-
<%=
|
|
3
|
+
<%= element_tag(:dialog, **dialog_attributes) do %>
|
|
4
4
|
<div data-slot="alert-dialog-header" class="<%= header_classes %>">
|
|
5
5
|
<% if media? %>
|
|
6
6
|
<div data-slot="alert-dialog-media" class="<%= media_classes %>"><%= media %></div>
|
|
@@ -36,6 +36,8 @@ module Poetry
|
|
|
36
36
|
"never a bare Dialog, never data-turbo-confirm.",
|
|
37
37
|
"with_title AND with_description are REQUIRED (both raise).",
|
|
38
38
|
"The action must be an explicit user activation - agents NEVER auto-submit the action.",
|
|
39
|
+
"with_action(submit: \"form-id\") makes the action that form's submit button (type=submit plus the " \
|
|
40
|
+
"form attribute - no form nests inside the dialog); the activation stays the user's.",
|
|
39
41
|
"No extra form fields inside an AlertDialog - if input is needed, use a Dialog.",
|
|
40
42
|
"Cancel keeps variant: :outline; do not make cancel visually primary."
|
|
41
43
|
].freeze
|
|
@@ -66,10 +68,20 @@ module Poetry
|
|
|
66
68
|
renders_one :media, doc: "Optional icon/illustration well above the title."
|
|
67
69
|
renders_one :action,
|
|
68
70
|
doc: "The confirming choice (required) - a Button; pass variant: :destructive for deletes. " \
|
|
69
|
-
"Activating it also closes the dialog (a caller-supplied data-action opts out)."
|
|
70
|
-
|
|
71
|
+
"Activating it also closes the dialog (a caller-supplied data-action opts out). " \
|
|
72
|
+
"submit: names the form it submits (type=submit plus the form attribute).",
|
|
73
|
+
renders: lambda { |submit: nil, **options, &block|
|
|
74
|
+
if submit && options[:href]
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"AlertDialog with_action(submit:) is a form's submit button - it takes no href:"
|
|
77
|
+
end
|
|
78
|
+
|
|
71
79
|
options[:data] =
|
|
72
80
|
{ slot: "alert-dialog-action", action: stimulus_action(:close) }.merge(options[:data] || {})
|
|
81
|
+
if submit
|
|
82
|
+
options[:type] = :submit
|
|
83
|
+
options[:form] = submit
|
|
84
|
+
end
|
|
73
85
|
Button::Component.new(**options, &block)
|
|
74
86
|
}
|
|
75
87
|
renders_one :cancel,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
<%=
|
|
2
|
-
<%=
|
|
3
|
-
<%=
|
|
1
|
+
<%= element_tag(:div, **root_attributes) do %>
|
|
2
|
+
<%= element_tag(:input, **input_attributes, class: Poetry::Ui::Input::Style.css) %>
|
|
3
|
+
<%= element_tag(:div, **content_attributes) do %>
|
|
4
4
|
<div data-slot="autocomplete-list" role="listbox" id="<%= list_id %>"
|
|
5
5
|
class="<%= Poetry::Ui::Command::Style.css(:list) %>"
|
|
6
6
|
<%= tag.attributes(stimulus_attributes_for(:list)) %>>
|
|
7
7
|
<% item_models.each_with_index do |item, index| %>
|
|
8
|
-
<%=
|
|
8
|
+
<%= element_tag(:div, **item_attributes(item, index)) do %>
|
|
9
9
|
<span><%= item.label %></span>
|
|
10
10
|
<% end %>
|
|
11
11
|
<% end %>
|
|
@@ -25,9 +25,16 @@ module Poetry
|
|
|
25
25
|
"empty_text: renders the no-matches state (hidden while anything matches).",
|
|
26
26
|
"open_on_focus: false waits for typing before suggesting.",
|
|
27
27
|
"Server-side filtering stays yours: render fewer items on re-render - the client " \
|
|
28
|
-
"filter only narrows what the server sent."
|
|
28
|
+
"filter only narrows what the server sent.",
|
|
29
|
+
"id: is the INPUT's id (a Field's label for= target); the root, list and item ids derive " \
|
|
30
|
+
"from it, and every aria-* attribute lands on the input (Field control_attributes splat in)."
|
|
29
31
|
].freeze
|
|
30
32
|
|
|
33
|
+
# The aria attributes a caller (or a Field's control_attributes)
|
|
34
|
+
# passes name the INPUT, not the root: pulled off the root in
|
|
35
|
+
# #initialize and merged into the input's attributes.
|
|
36
|
+
INPUT_ARIA = /\Aaria-/
|
|
37
|
+
|
|
31
38
|
use_stimulus do
|
|
32
39
|
on :root do
|
|
33
40
|
controller :autocomplete do
|
|
@@ -69,7 +76,8 @@ module Poetry
|
|
|
69
76
|
|
|
70
77
|
option :name, :string, required: true, doc: "The form param key; the input's text submits under it as-is."
|
|
71
78
|
option :value, :string, doc: "The initial input text."
|
|
72
|
-
option :id, :string, doc: "
|
|
79
|
+
option :id, :string, doc: "The input's DOM id (the Field label target); the root, list and item ids " \
|
|
80
|
+
"derive from it (-root, -list, -item-n)."
|
|
73
81
|
option :placeholder, :string, doc: "Placeholder text shown while the input is empty."
|
|
74
82
|
option :label, :string, doc: "The accessible name (or wire aria-labelledby via html attrs)."
|
|
75
83
|
option :empty_text, :string, default: "No results.",
|
|
@@ -125,19 +133,19 @@ module Poetry
|
|
|
125
133
|
# @api private
|
|
126
134
|
def item_models = (@item_models ||= [])
|
|
127
135
|
|
|
128
|
-
# The
|
|
136
|
+
# The input's id: the given id, else a stable instance id.
|
|
129
137
|
# @api private
|
|
130
|
-
def
|
|
131
|
-
@
|
|
132
|
-
"poetry-autocomplete-#{token}"
|
|
133
|
-
else
|
|
134
|
-
poetry_instance_id("poetry-autocomplete")
|
|
135
|
-
end
|
|
138
|
+
def input_id
|
|
139
|
+
@input_id ||= id.presence || poetry_instance_id("poetry-autocomplete")
|
|
136
140
|
end
|
|
137
141
|
|
|
138
|
-
# The
|
|
142
|
+
# The root id, derived from the input's.
|
|
143
|
+
# @api private
|
|
144
|
+
def autocomplete_id = "#{input_id}-root"
|
|
145
|
+
|
|
146
|
+
# The listbox's id, derived from the input's.
|
|
139
147
|
# @api private
|
|
140
|
-
def list_id = "#{
|
|
148
|
+
def list_id = "#{input_id}-list"
|
|
141
149
|
|
|
142
150
|
# The root's attributes: this component's markup over the core default.
|
|
143
151
|
def root_attributes
|
|
@@ -148,11 +156,12 @@ module Poetry
|
|
|
148
156
|
# @api private
|
|
149
157
|
def input_attributes
|
|
150
158
|
attrs = {
|
|
151
|
-
name: name, value: value, placeholder: placeholder, type: :text,
|
|
159
|
+
name: name, value: value, placeholder: placeholder, type: :text, id: input_id,
|
|
152
160
|
autocomplete: "off", role: "combobox",
|
|
153
161
|
"aria-autocomplete": "list", "aria-expanded": open.to_s, "aria-controls": list_id,
|
|
154
162
|
"data-slot": "autocomplete-input"
|
|
155
163
|
}
|
|
164
|
+
attrs.merge!(@input_aria)
|
|
156
165
|
attrs[:"aria-label"] = label if label.present?
|
|
157
166
|
attrs.merge(stimulus_attributes_for(:input).transform_keys(&:to_sym))
|
|
158
167
|
end
|
|
@@ -177,7 +186,7 @@ module Poetry
|
|
|
177
186
|
def item_attributes(item, index)
|
|
178
187
|
attrs = {
|
|
179
188
|
"class" => Poetry::Ui::Command::Style.css(:item), "data-slot" => "autocomplete-item",
|
|
180
|
-
"id" => "#{
|
|
189
|
+
"id" => "#{input_id}-item-#{index}", "role" => "option",
|
|
181
190
|
"data-label" => item.label
|
|
182
191
|
}
|
|
183
192
|
attrs["data-value"] = item.value if item.value.present?
|
|
@@ -191,7 +200,19 @@ module Poetry
|
|
|
191
200
|
# @api private
|
|
192
201
|
Item = Struct.new(:label, :value, :disabled, :highlighted, keyword_init: true)
|
|
193
202
|
|
|
194
|
-
private
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
# Pulls the aria-* attributes (flat, or nested aria: {}) off the
|
|
206
|
+
# root for the input - the Select trigger pattern, so a Field's
|
|
207
|
+
# control_attributes name the real control.
|
|
208
|
+
def initialize(**)
|
|
209
|
+
super
|
|
210
|
+
@input_aria = @html_attributes.keys.grep(INPUT_ARIA).to_h { |key| [key.to_sym, @html_attributes.delete(key)] }
|
|
211
|
+
nested = @html_attributes.delete("aria")
|
|
212
|
+
nested.each { |key, value| @input_aria[:"aria-#{key}"] = value } if nested.is_a?(Hash)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private :item_models, :input_id, :autocomplete_id, :list_id, :input_attributes, :content_attributes
|
|
195
216
|
private :item_attributes
|
|
196
217
|
end
|
|
197
218
|
end
|
|
@@ -20,7 +20,8 @@ module Poetry
|
|
|
20
20
|
|
|
21
21
|
# Projected into the registry, llms.txt, and the agent surface.
|
|
22
22
|
AGENT_RULES = [
|
|
23
|
-
"label: (the person's name) is
|
|
23
|
+
"label: (the person's name) is the avatar's accessible name (role=img); without one the avatar " \
|
|
24
|
+
"renders decorative (aria-hidden) - right beside the visible name, wrong on its own.",
|
|
24
25
|
"The content block is the fallback (initials) and is also required - it is what shows " \
|
|
25
26
|
"while the image loads or when it fails.",
|
|
26
27
|
"The badge slot is decorative (a presence dot); put the status meaning in label:, " \
|
|
@@ -31,15 +32,15 @@ module Poetry
|
|
|
31
32
|
renders_one :badge, doc: "Decorative presence dot, bottom-right; keep the status meaning in label:."
|
|
32
33
|
|
|
33
34
|
option :src, :string, doc: "The image URL; without it only the initials fallback shows."
|
|
34
|
-
option :label, :string,
|
|
35
|
-
|
|
36
|
-
"
|
|
35
|
+
option :label, :string, doc: "The person's name - the avatar's accessible name (role=img). Without one " \
|
|
36
|
+
"the avatar is decorative (aria-hidden, no role): use that beside the " \
|
|
37
|
+
"visible name, never for an avatar that stands alone."
|
|
37
38
|
option :size, :symbol, default: :default, doc: "The diameter axis."
|
|
38
39
|
|
|
39
40
|
validates :size, inclusion: { in: SIZES }
|
|
40
41
|
|
|
41
|
-
part "avatar", "Root span (role=img carrying the accessible name
|
|
42
|
-
"and badge layer inside it",
|
|
42
|
+
part "avatar", "Root span (role=img carrying the accessible name, or aria-hidden without label:) - " \
|
|
43
|
+
"fallback, image, and badge layer inside it",
|
|
43
44
|
states: {
|
|
44
45
|
"data-size" => { condition: "always - the resolved size", values: SIZES.map(&:to_s) }
|
|
45
46
|
}
|
|
@@ -49,11 +50,9 @@ module Poetry
|
|
|
49
50
|
"is given; a failed load paints nothing"
|
|
50
51
|
part "avatar-badge", "The decorative presence dot (the badge slot), bottom-right"
|
|
51
52
|
|
|
52
|
-
# Enforces
|
|
53
|
+
# Enforces the initials content block.
|
|
53
54
|
# @api private
|
|
54
55
|
def before_render
|
|
55
|
-
raise ArgumentError, "Avatar requires label: (the person's name - its accessible name)" if label.blank?
|
|
56
|
-
|
|
57
56
|
ensure_content!
|
|
58
57
|
end
|
|
59
58
|
|
|
@@ -65,14 +64,11 @@ module Poetry
|
|
|
65
64
|
end
|
|
66
65
|
end
|
|
67
66
|
|
|
68
|
-
# The root's attributes: this component's markup over the core
|
|
67
|
+
# The root's attributes: this component's markup over the core
|
|
68
|
+
# default - named (role=img) with a label, decorative without.
|
|
69
69
|
def root_attributes
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"data-size" => size,
|
|
73
|
-
"role" => "img", "aria-label" => label
|
|
74
|
-
}
|
|
75
|
-
)
|
|
70
|
+
naming = label.present? ? { "role" => "img", "aria-label" => label } : { "aria-hidden" => "true" }
|
|
71
|
+
super({ "data-size" => size }.merge(naming))
|
|
76
72
|
end
|
|
77
73
|
|
|
78
74
|
private
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%=
|
|
1
|
+
<%= element_tag(root_tag, **root_attributes) do %>
|
|
2
2
|
<% if loading %>
|
|
3
3
|
<span data-slot="spinner" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="animate-spin"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg></span>
|
|
4
4
|
<% end %>
|
|
@@ -54,7 +54,8 @@ module Poetry
|
|
|
54
54
|
option :type, :symbol, default: :button,
|
|
55
55
|
doc: "The native button type; ignored when the button renders as an anchor."
|
|
56
56
|
option :tag, :symbol, default: :button,
|
|
57
|
-
doc: "Renders the same styling on an <a> when :a - navigation wearing button clothes
|
|
57
|
+
doc: "Renders the same styling on an <a> when :a - navigation wearing button clothes; " \
|
|
58
|
+
"the anchor keeps its link semantics (no role)."
|
|
58
59
|
option :disabled, :boolean, default: false,
|
|
59
60
|
doc: "Disables the control (native disabled; aria-disabled on the anchor form)."
|
|
60
61
|
option :loading, :boolean, default: false,
|
|
@@ -139,15 +140,18 @@ module Poetry
|
|
|
139
140
|
end
|
|
140
141
|
|
|
141
142
|
# Native <button> gets native semantics (type, real disabled);
|
|
142
|
-
# `tag: :a` is navigation-styled-as-button:
|
|
143
|
-
#
|
|
143
|
+
# `tag: :a` is navigation-styled-as-button: a link that stays a
|
|
144
|
+
# link (no role - assistive tech announces the destination, and
|
|
145
|
+
# Enter, not Space, activates it) plus the faux-disabled
|
|
146
|
+
# convention (aria-disabled, href withheld). A caller's role:
|
|
147
|
+
# still lands through the passthrough.
|
|
144
148
|
def tag_attributes
|
|
145
149
|
link_tag? ? link_attributes : button_attributes
|
|
146
150
|
end
|
|
147
151
|
|
|
148
|
-
# The anchor's attributes: the
|
|
152
|
+
# The anchor's attributes: the href or the disabled state.
|
|
149
153
|
def link_attributes
|
|
150
|
-
attrs = {
|
|
154
|
+
attrs = {}
|
|
151
155
|
if disabled || loading
|
|
152
156
|
attrs["aria-disabled"] = true
|
|
153
157
|
elsif href.present?
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%=
|
|
1
|
+
<%= element_tag(:div, **root_attributes) do %>
|
|
2
2
|
<div data-slot="calendar-nav" class="<%= css(:nav) %>">
|
|
3
3
|
<%= render Poetry::Ui::Button::Component.new(**previous_options) do |button| %>
|
|
4
4
|
<% button.with_leading { render Poetry::Ui::Icon::Component.new(name: :"chevron-left") } %>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<% { "month" => [month_options, @month.month, t("poetry.calendar.month"), month_names_list[@month.month - 1]],
|
|
14
14
|
"year" => [year_options, @month.year, t("poetry.calendar.year"), @month.year.to_s] }.each do |unit, (options, current, label, text)| %>
|
|
15
15
|
<span data-slot="calendar-dropdown" data-calendar-unit="<%= unit %>" class="<%= css(:dropdown_root) %>"
|
|
16
|
-
<%= tag.attributes(
|
|
16
|
+
<%= tag.attributes(stimulus_attributes_for(:dropdown)) %>>
|
|
17
17
|
<span data-slot="calendar-caption-label" aria-hidden="true" class="<%= css(:dropdown_label) %>">
|
|
18
18
|
<span data-slot="calendar-dropdown-value"><%= text %></span>
|
|
19
19
|
<%= render Poetry::Ui::Icon::Component.new(name: :"chevron-down") %>
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
<% end %>
|
|
28
28
|
</div>
|
|
29
29
|
<% else %>
|
|
30
|
-
<div data-slot="calendar-caption" class="<%= css(:caption) %>" <%= tag.attributes(
|
|
30
|
+
<div data-slot="calendar-caption" class="<%= css(:caption) %>" <%= tag.attributes(stimulus_attributes_for(:caption)) %>><%= caption %></div>
|
|
31
31
|
<% end %>
|
|
32
32
|
<%= render Poetry::Ui::Button::Component.new(**next_options) do |button| %>
|
|
33
33
|
<% button.with_leading { render Poetry::Ui::Icon::Component.new(name: :"chevron-right") } %>
|
|
34
34
|
<% end %>
|
|
35
35
|
</div>
|
|
36
36
|
<div role="grid" data-slot="calendar-grid" class="<%= css(:grid) %>"
|
|
37
|
-
<%= tag.attributes(
|
|
37
|
+
<%= tag.attributes(stimulus_attributes_for(:grid)) %>>
|
|
38
38
|
<div role="row" data-slot="calendar-weekdays" class="<%= css(:weekdays) %>">
|
|
39
39
|
<% if week_numbers %>
|
|
40
40
|
<div role="columnheader" data-slot="calendar-week-number" class="<%= css(:weekday) %>"><span class="sr-only">Week</span></div>
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
<div role="rowheader" data-slot="calendar-week-number" class="<%= css(:weekday) %>"><%= iso_week(week) %></div>
|
|
50
50
|
<% end %>
|
|
51
51
|
<% week.each do |date| %>
|
|
52
|
-
<%=
|
|
53
|
-
<%=
|
|
52
|
+
<%= element_tag(:div, cell_attributes(date)) do %>
|
|
53
|
+
<%= element_tag(:button, day_attributes(date)) do %>
|
|
54
54
|
<span data-slot="calendar-day-label"><%= date.day %></span>
|
|
55
55
|
<% end %>
|
|
56
56
|
<% end %>
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
<% if name.present? %>
|
|
62
62
|
<% if range? %>
|
|
63
63
|
<input type="hidden" name="<%= name %>[start]" value="<%= range_start&.iso8601 %>"
|
|
64
|
-
<%= tag.attributes(
|
|
64
|
+
<%= tag.attributes(stimulus_attributes_for(:start_input)) %>>
|
|
65
65
|
<input type="hidden" name="<%= name %>[end]" value="<%= range_end&.iso8601 %>"
|
|
66
|
-
<%= tag.attributes(
|
|
66
|
+
<%= tag.attributes(stimulus_attributes_for(:end_input)) %>>
|
|
67
67
|
<% else %>
|
|
68
68
|
<input type="hidden" name="<%= name %>" value="<%= selected&.iso8601 %>"
|
|
69
|
-
<%= tag.attributes(
|
|
69
|
+
<%= tag.attributes(stimulus_attributes_for(:hidden_input)) %>>
|
|
70
70
|
<% end %>
|
|
71
71
|
<% end %>
|
|
72
72
|
<% end %>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
<%=
|
|
1
|
+
<%= element_tag(:div, **root_attributes) do %>
|
|
2
2
|
<% if header? %>
|
|
3
3
|
<div data-slot="card-header" class="<%= css(:header, class: header_class) %>">
|
|
4
|
-
<% if title? %><%=
|
|
4
|
+
<% if title? %><%= element_tag(title_tag, title, "data-slot" => "card-title", class: css(:title)) %><% end %>
|
|
5
5
|
<% if description? %><div data-slot="card-description" class="<%= css(:description) %>"><%= description %></div><% end %>
|
|
6
6
|
<% if action? %><div data-slot="card-action" class="<%= css(:action) %>"><%= action %></div><% end %>
|
|
7
7
|
</div>
|
|
8
8
|
<% end %>
|
|
9
|
-
|
|
10
|
-
<% if footer? %><%=
|
|
9
|
+
<% if content.present? %><div data-slot="card-content" class="<%= css(:content, class: content_class) %>"><%= content %></div><% end %>
|
|
10
|
+
<% if footer? %><%= element_tag(:div, footer_attributes) do %><%= capture(&footer_block) if footer_block %><% end %><% end %>
|
|
11
11
|
<% end %>
|
|
@@ -56,7 +56,8 @@ module Poetry
|
|
|
56
56
|
part "card-title", "The heading (title_tag, h3 by default)"
|
|
57
57
|
part "card-description", "Muted one-liner under the title"
|
|
58
58
|
part "card-action", "The header's trailing corner control"
|
|
59
|
-
part "card-content", "The body - the content block
|
|
59
|
+
part "card-content", "The body - renders when the content block gives content (a header-and-footer " \
|
|
60
|
+
"card has no empty body cell)"
|
|
60
61
|
part "card-footer", "The bottom row (actions/meta)"
|
|
61
62
|
|
|
62
63
|
# The caller's footer block, captured by with_footer.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<%=
|
|
2
|
-
<%=
|
|
1
|
+
<%= element_tag(:div, **root_attributes) do %>
|
|
2
|
+
<%= element_tag(:div, viewport_attributes) do %>
|
|
3
3
|
<div class="<%= css(:track, class: [css(vertical? ? :track_vertical : :track_horizontal), track_classes].compact) %>">
|
|
4
4
|
<% slides.each do |slide| %>
|
|
5
|
-
<%=
|
|
5
|
+
<%= element_tag(:div, capture(&slide.block), item_attributes(slide)) %>
|
|
6
6
|
<% end %>
|
|
7
7
|
</div>
|
|
8
8
|
<% end %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%# Fragment - button + input(s) as SIBLINGS, no wrapper: the source's
|
|
2
2
|
`peer` class contract (labels style via peer-*) breaks if poetry wraps.
|
|
3
3
|
The controller lives on the button; the input resolves by stable id. %>
|
|
4
|
-
<%=
|
|
4
|
+
<%= element_tag(:button, **root_attributes) do %>
|
|
5
5
|
<span data-slot="checkbox-indicator" <% if indeterminate? %>data-indeterminate=""<% elsif checked? %>data-checked=""<% else %>data-unchecked=""<% end %> aria-hidden="true" class="<%= css(:indicator) %>">
|
|
6
6
|
<%# MinusIcon on indeterminate is a POETRY ADDITION (shadcn renders a
|
|
7
7
|
check for mixed state - a real gap). %>
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
<%# Rails parity hidden FIRST (last-wins ordering, Tags::CheckBox-exact)
|
|
13
13
|
so unchecked SUBMITS unchecked_value; disabled fields submit nothing. %>
|
|
14
14
|
<% if unchecked_value %>
|
|
15
|
-
<%=
|
|
15
|
+
<%= element_tag(:input, type: "hidden", name: name, value: unchecked_value, disabled: disabled) %>
|
|
16
16
|
<% end %>
|
|
17
17
|
<%# THE form participant + the store: input.indeterminate is a JS-only
|
|
18
18
|
property - the controller derives it from the checked attributes on connect. %>
|
|
19
|
-
<%=
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
<%= element_tag(:input, type: "checkbox", id: input_id, name: name, value: value, checked: checked?,
|
|
20
|
+
disabled: disabled, "data-slot": "checkbox-input", "aria-hidden": true,
|
|
21
|
+
tabindex: "-1", class: css(:input)) %>
|
|
22
22
|
<% end %>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<%# InputGroup anatomy: the readonly mono control, trailing copy
|
|
2
2
|
affordance with stacked copy/check glyphs swapping on data-copied. %>
|
|
3
|
-
<%=
|
|
4
|
-
<%=
|
|
5
|
-
<%=
|
|
6
|
-
<%=
|
|
3
|
+
<%= element_tag(:div, **root_attributes) do %>
|
|
4
|
+
<%= element_tag(:div, **group_attributes) do %>
|
|
5
|
+
<%= element_tag(:input, **input_attributes) %>
|
|
6
|
+
<%= element_tag(:div, **addon_attributes) do %>
|
|
7
7
|
<%= render copy_button do %>
|
|
8
8
|
<span class="<%= css(:icon_stack) %>">
|
|
9
9
|
<%= render Poetry::Ui::Icon::Component.new(name: :copy, class: css(:icon_copy)) %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%# The pre/code nesting stays on one line - literal template whitespace
|
|
2
2
|
inside <pre> would render. %>
|
|
3
|
-
<%=
|
|
4
|
-
<%=
|
|
3
|
+
<%= element_tag(:div, **root_attributes) do %>
|
|
4
|
+
<%= element_tag(:pre, element_tag(:code, highlighted || code, **code_attributes), **pre_attributes) %>
|
|
5
5
|
<% if copy %>
|
|
6
6
|
<%= render copy_button do %>
|
|
7
7
|
<span class="<%= css(:icon_stack) %>">
|
|
@@ -21,7 +21,8 @@ module Poetry
|
|
|
21
21
|
# Projected into the registry, llms.txt, and the agent surface.
|
|
22
22
|
AGENT_RULES = [
|
|
23
23
|
ComposableTrigger::AGENT_RULE,
|
|
24
|
-
"The trigger is with_trigger { \"label\" } - a
|
|
24
|
+
"The trigger is with_trigger { \"label\" } - a poetry Button (keywords are Button props: variant:, " \
|
|
25
|
+
"size:), wired for you (aria-expanded/controls).",
|
|
25
26
|
"Server-render the initial state via open: - never toggle data-open/data-closed by hand.",
|
|
26
27
|
"Content stays in the DOM when closed (hidden) - do not conditionally render it.",
|
|
27
28
|
"For URL-controlled disclosure without JS, render open: from params - the same markup serves both."
|
|
@@ -31,16 +32,20 @@ module Poetry
|
|
|
31
32
|
# a missing trigger without rendering.
|
|
32
33
|
REQUIRED_SLOTS = { trigger: "the disclosure control" }.freeze
|
|
33
34
|
|
|
35
|
+
# The class the trigger slot renders - with_trigger composes a
|
|
36
|
+
# Button, so callers get Button's full option contract.
|
|
37
|
+
SLOT_RENDERS = { trigger: Button::Component }.freeze
|
|
38
|
+
|
|
34
39
|
renders_one :trigger,
|
|
35
|
-
doc: "The disclosure control - a
|
|
36
|
-
"
|
|
40
|
+
doc: "The disclosure control - a composed Button (keywords are Button props), wired for you " \
|
|
41
|
+
"(aria-expanded, aria-controls).",
|
|
37
42
|
renders: lambda { |**options, &block|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
wiring = {
|
|
44
|
+
"data-slot" => "collapsible-trigger",
|
|
40
45
|
"aria-expanded" => open.to_s, "aria-controls" => content_id
|
|
41
46
|
}.merge(stimulus_attributes_for(:trigger))
|
|
42
|
-
composed_trigger(
|
|
43
|
-
|
|
47
|
+
composed_trigger(wiring, options, &block) ||
|
|
48
|
+
Button::Component.new(**wiring, **options, &block)
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
use_stimulus do
|
|
@@ -66,10 +71,12 @@ module Poetry
|
|
|
66
71
|
"data-open" => "expanded (server-rendered from open:; the controller flips the pair at runtime)",
|
|
67
72
|
"data-closed" => "collapsed (the server-rendered default)"
|
|
68
73
|
}
|
|
69
|
-
part "collapsible-trigger", "The disclosure button - mirrors aria-expanded",
|
|
74
|
+
part "collapsible-trigger", "The disclosure button - a composed Button - mirrors aria-expanded",
|
|
70
75
|
states: {
|
|
71
76
|
"data-panel-open" => "its content is open (controller-written; " \
|
|
72
|
-
"absent while closed)"
|
|
77
|
+
"absent while closed)",
|
|
78
|
+
"data-variant" => "always - the composed Button's variant axis",
|
|
79
|
+
"data-size" => "always - the composed Button's size axis"
|
|
73
80
|
}
|
|
74
81
|
part "collapsible-content", "The disclosure panel - stays in the DOM when closed (hidden) " \
|
|
75
82
|
"and rides the presence helper on exit",
|
|
@@ -7,14 +7,14 @@ module Poetry
|
|
|
7
7
|
class Preview < Poetry::Core::Preview::Base
|
|
8
8
|
def default
|
|
9
9
|
render_component do |collapsible|
|
|
10
|
-
collapsible.with_trigger(
|
|
10
|
+
collapsible.with_trigger(variant: :ghost, size: :sm) { "Show details" }
|
|
11
11
|
tag.div("Hidden until disclosed.", class: "pt-2 text-sm text-muted-foreground")
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def initially_open
|
|
16
16
|
render_component(open: true) do |collapsible|
|
|
17
|
-
collapsible.with_trigger(
|
|
17
|
+
collapsible.with_trigger(variant: :ghost, size: :sm) { "Hide details" }
|
|
18
18
|
tag.div("Server-rendered open.", class: "pt-2 text-sm text-muted-foreground")
|
|
19
19
|
end
|
|
20
20
|
end
|