poetry-ui 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -1
- data/README.md +2 -2
- data/THIRD_PARTY_NOTICES.md +29 -0
- data/app/components/poetry/ui/combobox/component.rb +4 -1
- data/app/components/poetry/ui/command/component.rb +3 -2
- data/app/components/poetry/ui/context_menu/component.rb +4 -1
- data/app/components/poetry/ui/dialog/component.rb +1 -1
- data/app/components/poetry/ui/drawer/component.rb +3 -0
- data/app/components/poetry/ui/dropdown_menu/component.rb +5 -1
- data/app/components/poetry/ui/hover_card/component.rb +3 -2
- data/app/components/poetry/ui/menubar/component.rb +4 -3
- data/app/components/poetry/ui/menus.rb +21 -2
- data/app/components/poetry/ui/message_scroller/component.rb +28 -4
- data/app/components/poetry/ui/message_scroller/style.rb +7 -1
- data/app/components/poetry/ui/navigation_menu/component.rb +26 -10
- data/app/components/poetry/ui/number_field/component.rb +0 -1
- data/app/components/poetry/ui/select/component.rb +4 -2
- data/app/components/poetry/ui/toast_trigger/component.rb +4 -3
- data/app/controllers/poetry/ui/llms_controller.rb +5 -1
- data/app/helpers/poetry/ui/form_builder/type_inference.rb +5 -3
- data/app/helpers/poetry/ui/form_builder.rb +3 -0
- data/config/component_registry.yml +124 -12
- data/lib/generators/poetry/agents_section.rb +58 -21
- data/lib/generators/poetry/install/install_generator.rb +226 -16
- data/lib/generators/poetry/scaffold_templates/scaffold_templates_generator.rb +5 -3
- data/lib/generators/poetry/scaffold_templates/templates/_form.html.erb.tt +10 -56
- data/lib/generators/poetry/skill/templates/poetry-component/SKILL.md +25 -0
- data/lib/generators/poetry/skill/templates/poetry-design/references/audit.md +5 -3
- data/lib/generators/poetry/skills_section.rb +33 -5
- data/lib/poetry/ui/chat.rb +5 -0
- data/lib/poetry/ui/reset_floor.rb +199 -0
- data/lib/poetry/ui/themes.rb +1 -1
- data/lib/poetry/ui/version.rb +1 -1
- data/lib/poetry/ui.rb +51 -6
- data/lib/tasks/poetry/check.rake +71 -15
- data/lib/tasks/poetry/design.rake +12 -1
- data/lib/tasks/poetry/registry.rake +22 -0
- data/lib/tasks/poetry/stimulus.rake +29 -0
- data/lib/tasks/poetry/verify.rake +18 -1
- data/reset/reset.css +154 -0
- data/themes/default.css +0 -11
- data/themes/luma.css +1 -12
- data/themes/lyra.css +1 -12
- data/themes/maia.css +1 -12
- data/themes/mira.css +1 -12
- data/themes/nova.css +1 -12
- data/themes/rhea.css +1 -12
- data/themes/sera.css +1 -12
- data/themes/vega.css +1 -12
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfaee0a24a3a61b10a2bf12f2afb0dd5a0b8f93117392f8deb4fbb81da36c984
|
|
4
|
+
data.tar.gz: bf5011451e6d6bd7fe2c7bed1ebc1150f3503d04fc04687f8bd0463d6a1612e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11db0fc73021f75632fbf927e3f807043c7091eeaa049b5d58b90561910e2d521a94abe0f871333484f9f0292e3cc2f25c262c578d4dfb5d43a12591cb33159a
|
|
7
|
+
data.tar.gz: d27e5cb320b82e7f40fca65b3e3898350a5ec6ac8778c88b95fb7f8a11fca29861783a1bc016322e5dab9a583b82586fccc7fd48d905f4f0603029fed700f866
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.1.2] - 2026-09-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- The install names the theme it puts in the slot: on a first install or an explicit `--theme`, one line says which theme fills `poetry/style-default.css`, that the slot name never changes, and that the file's first line names the theme. The Theming and Installation guides say the same, for the reader who opens a file called default and finds vega.
|
|
8
|
+
|
|
9
|
+
- `poetry:install` and `poetry:skill` say so when the host gitignores `.claude/skills`: the skills still serve that checkout, and the note names the refresh command teammates and CI run (`bin/rails g poetry:skill`) or the alternative of un-ignoring the directory. Asked of git only; no git, no repository, or a tracked directory stay silent.
|
|
10
|
+
|
|
11
|
+
- App components are first-class on the booted surfaces. A component the app defines on the DSL with `helper :name` renders through that helper, `poetry:check` lints it under that name with its own contract, `/poetry/llms.txt` and `llms-full.txt` list it under an App components section with its agent rules, and `poetry:skill` writes a `references/app.md` with a menu line. The AGENTS.md section and the poetry-component skill teach the declaration. The MCP server's `check` tool learns the declared helpers from source, so it agrees the helper exists; its contracts and the runtime skill map stay gem-only (boot-free by design).
|
|
12
|
+
|
|
13
|
+
- `poetry:install --no-preflight`, for an app that imports Tailwind without preflight: vendors the reset floor (`poetry/reset.css`, Tailwind's preflight at zero specificity, generated from the pinned binary and gated against it) into `layer(base)` ahead of the theme, and on a fresh entry writes the split no-preflight import. A plain re-run keeps the floor once installed. Measured against the golden corpus: without preflight two thirds of the components render differently even behind a modern reset, so the floor is what keeps them whole; `POETRY_PREFLIGHT=0 rake test:visual` proves it. The installer and `poetry:check` warn (`preflight-missing`) when an entry imports neither preflight nor the floor.
|
|
14
|
+
- `poetry:design:overrides` counts only a rule against a theme-owned `cn-*` name (a gem dictionary's or the installed fragment's own) as an override. A host kit's own `cn-*` dictionary, or a page helper class in that namespace, is no longer reported as undeclared drift; a blanket declaration written to silence those now shows as stale.
|
|
15
|
+
- `bin/rails poetry:stimulus:manifest` writes the app's controllers manifest (`config/controllers_manifest.json`, the file a gem commits) from `app/javascript/controllers`; registered at boot, a host controller validates like poetry's (the DSL by Symbol at class load, template wiring in `poetry:check`, its API in the registry). A controller the reader cannot describe is named and stays unvalidated until its entry is written by hand in the same file, where regeneration keeps it. `poetry:check` warns (`manifest-stale`) and `poetry:verify` fails when the entries the reader produced no longer match the sources; hand-written entries never count.
|
|
16
|
+
- `bin/rails poetry:registry` writes the app's own components to `config/component_registry.yml`, the path a gem's registry lives at, so the MCP server and the runtime skill find them boot-free with full contracts and agent rules. Opt-in; once written, `poetry:check` warns (`registry-stale`) and `poetry:verify` fails when the file no longer matches the classes.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The reset floor's header and the Installation guide say exactly what wins: an unlayered host rule always, a host rule in `layer(base)` when it comes after the floor's import, and `[hidden]` keeps preflight's `!important` on purpose. `poetry:verify` names the remedy when a kit's own `cn-*` class is not in the compiled CSS. The AGENTS.md section spells JSON output as `POETRY_CHECK_JSON=1`.
|
|
21
|
+
|
|
22
|
+
- The upgrade path holds on real hosts: `--charts` is remembered by the vendored `poetry/charts.css`, so a plain re-run refreshes the chart files and a theme switch swaps the chart fragment with the theme; the superseded 0.1.1 tokens import converges however it was written (indented, annotated, or with a carriage return, which it keeps); every `eval_gemfile` spelling is read before `herb` is added; the Claude Code skills are refreshed like the vendored CSS, without an overwrite prompt (`--skip` keeps a hand-edited one); an AGENTS.md begin marker without its end is an error naming the file, a byte-identical section reports `identical`, the section is written in the file's own line endings, and only the first section is replaced.
|
|
23
|
+
|
|
24
|
+
- `poetry:check` hands the runner the app root, so the mailer exemption and the design tier judge a template by its path inside the app; an unreadable file is a warning in the report, not a crash of the task.
|
|
25
|
+
|
|
26
|
+
- `poetry:check` reports `registry-invalid` when `config/component_registry.yml` exists but is not a registry (empty, malformed, the wrong shape), naming `bin/rails poetry:registry` as the fix; the committed registry regenerates with every helper named.
|
|
27
|
+
|
|
28
|
+
- The scaffold form template is the form builder's: `form_with(model:, builder: Poetry::Ui::FormBuilder)` and one `f.input` per attribute, the control inferred from the column, the name and the validations, with the template adding only what the generator knows and the model cannot say (`as: :text` for rich text, `multiple: true` for many attachments, `required: true` from `null: false`). The gem's reference CRUD form now follows the gem's own first rule about forms. A host that installed the templates before keeps its copy (re-runs never overwrite); delete `lib/templates/erb/scaffold/_form.html.erb.tt` and re-run `poetry:scaffold_templates` for the builder version. On the way, two inference gaps in the builder: `has_many_attached` attributes infer a file input (the plural responder), and `website` / `permalink` infer a URL input like `url` does.
|
|
29
|
+
|
|
30
|
+
- The form builder's agent rules say which parts a builder field renders (`field-label`, `field-description` for the hint, `field-error`), so llms.txt's Forms section, the registry, the MCP server and the skill all name the slots a wiring test asserts on; the Testing guide says the same.
|
|
31
|
+
|
|
32
|
+
- `poetry:check` treats mailer templates as email, not pages: under a `*_mailer/` directory or in the mailer layout the raw-color rule stays quiet (an inline hex is the only paint a mail client honours) and the design tier skips them; every other rule runs there as before. The AGENTS.md section says so.
|
|
33
|
+
|
|
34
|
+
- An off-list variant or a missing required option now raises at construction in development and test (poetry-core's runtime values tier) instead of rendering unstyled; the AGENTS.md section says so beside the check.
|
|
35
|
+
- poetry-ui pins its components to `css_mode :tailwind`, and the class-name merger follows: a host's global `BemMerger` (for a kit of its own) never reaches poetry-ui's utility conflict resolution. A host's global `css_mode = :bem` (for a kit of its own on the DSL) no longer strips poetry-ui's styling; the two render side by side. The seeded initializer's comment says so instead of offering `:bem` as a poetry-ui option.
|
|
36
|
+
- `poetry:check` and the AGENTS.md census find registry roots by convention (every loaded engine with a published registry, then the app's own file) and name no gem; poetry-charts is no longer special-cased, and any engine built on the DSL that commits a registry joins the check the same way.
|
|
37
|
+
- `poetry:install` imports `tokens.css` into `layer(theme)`, so a token the host already declares (`--primary`, `--accent`, `--muted`, ...) keeps its value whatever the order in the Tailwind entry, and that value now reaches Poetry's components too. Before, the appended import landed after the host's own `:root` and took the same names over: an app's brand color vanished before a single Poetry component rendered. A re-run rewrites the earlier unlayered import line in place.
|
|
38
|
+
- `poetry:install` reports, before writing anything, every Poetry token name and theme key the app's stylesheets already declare, with the file and line and what Poetry paints with that role. It never blocks. `poetry:check` repeats the report as `token-collision` warnings.
|
|
39
|
+
|
|
40
|
+
- The WebMCP agent-focus rules (`:tool-form-active`, `:tool-submit-active`) leave the nine theme fragments, where they were identical, for poetry-agent's own stylesheet; `poetry:install` vendors it into `layer(base)` when that gem is bundled. A host without poetry-agent no longer carries them, nor the two warnings the CSS optimizer printed for the origin-trial pseudo-classes on every minified build (the Rails Tailwind task minifies by default, so every build and watch run).
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `poetry:install --force` no longer rewrites the four files the install seeds once for the host to own (`config/poetry_components.yml`, the initializer, `poetry/base.css`, `poetry/typeset.css`); a re-run with the flag used to reset a host's override declarations and settings to the seed. The vendored artifacts refresh on every run, so the flag is never needed on an upgrade.
|
|
45
|
+
- `poetry:install` no longer adds a second `herb` declaration when the gem is declared in a file the Gemfile pulls in with `eval_gemfile` (a shared Gemfile).
|
|
46
|
+
|
|
47
|
+
## [0.1.1] - 2026-09-08
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `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.
|
|
52
|
+
- 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.
|
|
53
|
+
- `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.
|
|
54
|
+
- `poetry:install --skip-bundle`.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- `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.
|
|
59
|
+
- 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.
|
|
60
|
+
- 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.
|
|
61
|
+
- 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.
|
|
62
|
+
- 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.
|
|
63
|
+
- ToastTrigger names its Button root through `identity:`.
|
|
64
|
+
- NavigationMenu's agent rules state the keyboard contract: ArrowLeft/ArrowRight between triggers and links, ArrowDown opens the focused trigger's panel, Escape closes.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- HoverCard: a tap on the trigger clicks through on touch devices (the collapsed sidebar's icons did nothing on a tablet).
|
|
69
|
+
- NumberField: Tab's select-all and a click's caret placement stay native.
|
|
4
70
|
|
|
5
71
|
## [0.1.0] - 2026-09-05
|
|
6
72
|
|
data/README.md
CHANGED
|
@@ -59,8 +59,8 @@ bin/rails g poetry:add <name> # adds newly-shipped files, never overwrites
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
And `rails g scaffold` itself can produce poetry-composed views — a
|
|
62
|
-
DataTable index with sanitized URL state,
|
|
63
|
-
matching controller — by installing the app-owned template overrides once:
|
|
62
|
+
DataTable index with sanitized URL state, forms on the poetry form
|
|
63
|
+
builder, and a matching controller — by installing the app-owned template overrides once:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
66
|
bin/rails g poetry:scaffold_templates
|
data/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -33,6 +33,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
33
33
|
SOFTWARE.
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Tailwind CSS
|
|
37
|
+
|
|
38
|
+
- Source: https://github.com/tailwindlabs/tailwindcss (MIT)
|
|
39
|
+
- `reset/reset.css` (the reset floor `poetry:install --no-preflight` vendors) is preflight, generated from the pinned tailwindcss binary with every selector wrapped in `:where()`
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
MIT License
|
|
43
|
+
|
|
44
|
+
Copyright (c) Tailwind Labs, Inc.
|
|
45
|
+
|
|
46
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
47
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
48
|
+
in the Software without restriction, including without limitation the rights
|
|
49
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
50
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
51
|
+
furnished to do so, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
The above copyright notice and this permission notice shall be included in all
|
|
54
|
+
copies or substantial portions of the Software.
|
|
55
|
+
|
|
56
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
57
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
58
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
59
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
60
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
61
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
62
|
+
SOFTWARE.
|
|
63
|
+
```
|
|
64
|
+
|
|
36
65
|
## Base UI
|
|
37
66
|
|
|
38
67
|
- Source: https://github.com/mui/base-ui (MIT)
|
|
@@ -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
|
|
@@ -169,7 +169,7 @@ module Poetry
|
|
|
169
169
|
# block--modifier tokens ARE the host's styling contract.
|
|
170
170
|
# @api private
|
|
171
171
|
def html_attributes
|
|
172
|
-
return super unless
|
|
172
|
+
return super unless css_mode == :tailwind
|
|
173
173
|
|
|
174
174
|
@html_attributes.merge(class: classnames(@html_attributes[:class]))
|
|
175
175
|
end
|
|
@@ -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
|
|
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 :
|
|
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
|
-
|
|
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
|
-
"
|
|
46
|
-
|
|
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
|
-
}
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
@@ -21,7 +21,11 @@ module Poetry
|
|
|
21
21
|
# The shared builder (the llms construction rule, extended): the
|
|
22
22
|
# served text uses the exact registry construction the committed
|
|
23
23
|
# file is generated from - helpers and blocks sections included.
|
|
24
|
-
|
|
24
|
+
# The app's own components ride along (their section follows the
|
|
25
|
+
# gem catalog), so an agent reading llms-full.txt sees their agent
|
|
26
|
+
# rules and helpers too.
|
|
27
|
+
Poetry::Core::LlmsText.new(registry: Poetry::Ui.registry,
|
|
28
|
+
host_registry: Poetry::Core::HostComponents.registry(root: Rails.root))
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
@@ -17,7 +17,7 @@ module Poetry
|
|
|
17
17
|
STRING_HEURISTICS = {
|
|
18
18
|
/password/ => :password,
|
|
19
19
|
/email/ => :email,
|
|
20
|
-
/\burl\b|_url\b/ => :url,
|
|
20
|
+
/\burl\b|_url\b|website|permalink/ => :url,
|
|
21
21
|
/phone|\btel\b/ => :tel,
|
|
22
22
|
/search|query/ => :search
|
|
23
23
|
}.freeze
|
|
@@ -43,9 +43,11 @@ module Poetry
|
|
|
43
43
|
COLUMN_TYPES.fetch(column, :string)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# has_one_attached answers to <name>_attachment, has_many_attached
|
|
47
|
+
# to <name>_attachments; the other two are the classic uploaders.
|
|
46
48
|
def attachment_attribute?(method)
|
|
47
|
-
object.respond_to?("#{method}_attachment") || object.respond_to?("#{method}
|
|
48
|
-
object.respond_to?("remote_#{method}_url")
|
|
49
|
+
object.respond_to?("#{method}_attachment") || object.respond_to?("#{method}_attachments") ||
|
|
50
|
+
object.respond_to?("#{method}_attacher") || object.respond_to?("remote_#{method}_url")
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def enum_attribute?(method)
|
|
@@ -32,6 +32,9 @@ module Poetry
|
|
|
32
32
|
"overrides the presence inference (aria only).",
|
|
33
33
|
"Hints/placeholders resolve from poetry_form.* i18n (simple_form.* keys keep working " \
|
|
34
34
|
"as a fallback); pass hint:/placeholder: to override.",
|
|
35
|
+
"Every builder field renders a Field, so wiring tests assert on its parts: the label is " \
|
|
36
|
+
"data-slot=field-label, the hint data-slot=field-description, the error data-slot=field-error, " \
|
|
37
|
+
"with the control inside (never a bare hint or description slot).",
|
|
35
38
|
"f.submit renders a poetry Button with the Rails i18n label; f.fieldset(legend:)/" \
|
|
36
39
|
"f.group lay out sections; boolean f.input renders the horizontal Field " \
|
|
37
40
|
"(switch: true -> the setting row).",
|