poetry-core 0.0.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 +7 -0
- data/CHANGELOG.md +3 -0
- data/DESIGN.md +145 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/THIRD_PARTY_NOTICES.md +507 -0
- data/app/components/poetry/core/box/component.rb +82 -0
- data/app/components/poetry/core/component.rb +390 -0
- data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
- data/app/components/poetry/core/concerns/introspection.rb +533 -0
- data/app/components/poetry/core/concerns/options.rb +262 -0
- data/app/components/poetry/core/concerns/parts.rb +148 -0
- data/app/components/poetry/core/concerns/stimulus.rb +363 -0
- data/app/components/poetry/core/concerns/styles.rb +323 -0
- data/app/components/poetry/core/style.rb +150 -0
- data/app/components/poetry/core/wrapper/component.rb +90 -0
- data/app/javascript/poetry/core/accordion_controller.js +165 -0
- data/app/javascript/poetry/core/action_bar_controller.js +109 -0
- data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
- data/app/javascript/poetry/core/calendar_controller.js +406 -0
- data/app/javascript/poetry/core/carousel_controller.js +167 -0
- data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
- data/app/javascript/poetry/core/checked_controller.js +208 -0
- data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
- data/app/javascript/poetry/core/combobox_controller.js +1095 -0
- data/app/javascript/poetry/core/command_controller.js +510 -0
- data/app/javascript/poetry/core/context_menu_controller.js +204 -0
- data/app/javascript/poetry/core/date_field_controller.js +584 -0
- data/app/javascript/poetry/core/date_picker_controller.js +111 -0
- data/app/javascript/poetry/core/deferred_controller.js +111 -0
- data/app/javascript/poetry/core/dialog_controller.js +206 -0
- data/app/javascript/poetry/core/dismissable_controller.js +182 -0
- data/app/javascript/poetry/core/drawer_controller.js +365 -0
- data/app/javascript/poetry/core/file_input_controller.js +191 -0
- data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
- data/app/javascript/poetry/core/helpers/announce.js +228 -0
- data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
- data/app/javascript/poetry/core/helpers/collection.js +18 -0
- data/app/javascript/poetry/core/helpers/direction.js +15 -0
- data/app/javascript/poetry/core/helpers/escape.js +38 -0
- data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
- data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
- data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
- data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
- data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
- data/app/javascript/poetry/core/helpers/mask.js +283 -0
- data/app/javascript/poetry/core/helpers/portal.js +217 -0
- data/app/javascript/poetry/core/helpers/presence.js +210 -0
- data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
- data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
- data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
- data/app/javascript/poetry/core/helpers/state.js +80 -0
- data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
- data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
- data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
- data/app/javascript/poetry/core/hotkey_controller.js +55 -0
- data/app/javascript/poetry/core/hover_card_controller.js +431 -0
- data/app/javascript/poetry/core/index.js +218 -0
- data/app/javascript/poetry/core/mask_controller.js +500 -0
- data/app/javascript/poetry/core/menu_controller.js +1050 -0
- data/app/javascript/poetry/core/menubar_controller.js +329 -0
- data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
- data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
- data/app/javascript/poetry/core/number_field_controller.js +404 -0
- data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
- data/app/javascript/poetry/core/otp_controller.js +219 -0
- data/app/javascript/poetry/core/popover_controller.js +300 -0
- data/app/javascript/poetry/core/popper_controller.js +439 -0
- data/app/javascript/poetry/core/pressed_controller.js +74 -0
- data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
- data/app/javascript/poetry/core/radio_group_controller.js +197 -0
- data/app/javascript/poetry/core/resizable_controller.js +202 -0
- data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
- data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
- data/app/javascript/poetry/core/search_field_controller.js +95 -0
- data/app/javascript/poetry/core/select_controller.js +924 -0
- data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
- data/app/javascript/poetry/core/sheet_controller.js +24 -0
- data/app/javascript/poetry/core/sidebar_controller.js +272 -0
- data/app/javascript/poetry/core/slider_controller.js +419 -0
- data/app/javascript/poetry/core/state_controller.js +82 -0
- data/app/javascript/poetry/core/table_selection_controller.js +153 -0
- data/app/javascript/poetry/core/tabs_controller.js +151 -0
- data/app/javascript/poetry/core/tag_group_controller.js +134 -0
- data/app/javascript/poetry/core/toast_controller.js +228 -0
- data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
- data/app/javascript/poetry/core/toaster_controller.js +197 -0
- data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
- data/app/javascript/poetry/core/tooltip_controller.js +541 -0
- data/app/javascript/poetry/core/tree_controller.js +265 -0
- data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
- data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
- data/app/views/poetry/core/preview.html.erb +13 -0
- data/config/component_registry.yml +20 -0
- data/config/controllers_manifest.json +1528 -0
- data/config/importmap.rb +11 -0
- data/config/state_vocabulary.json +31 -0
- data/lib/active_model/type/list.rb +33 -0
- data/lib/active_model/type/symbol.rb +36 -0
- data/lib/poetry/core/check/stable_identity.rb +104 -0
- data/lib/poetry/core/check.rb +1445 -0
- data/lib/poetry/core/config.rb +286 -0
- data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
- data/lib/poetry/core/css/bem_merger.rb +45 -0
- data/lib/poetry/core/css/bem_reference.rb +87 -0
- data/lib/poetry/core/css/override_scan.rb +110 -0
- data/lib/poetry/core/css/resolver.rb +161 -0
- data/lib/poetry/core/css/safelist.rb +41 -0
- data/lib/poetry/core/css/tailwind_merger.rb +102 -0
- data/lib/poetry/core/css/template_classes.rb +102 -0
- data/lib/poetry/core/css/theme_coverage.rb +72 -0
- data/lib/poetry/core/css/var_coverage.rb +75 -0
- data/lib/poetry/core/css/verifier.rb +106 -0
- data/lib/poetry/core/design_lint.rb +720 -0
- data/lib/poetry/core/design_md/import.rb +250 -0
- data/lib/poetry/core/design_md.rb +461 -0
- data/lib/poetry/core/engine.rb +71 -0
- data/lib/poetry/core/errors.rb +53 -0
- data/lib/poetry/core/html/attributes.rb +667 -0
- data/lib/poetry/core/icons.rb +157 -0
- data/lib/poetry/core/llms_text.rb +354 -0
- data/lib/poetry/core/page_architectures.rb +304 -0
- data/lib/poetry/core/part_contract.rb +285 -0
- data/lib/poetry/core/preview/abstract.rb +121 -0
- data/lib/poetry/core/preview/base.rb +227 -0
- data/lib/poetry/core/preview/sidecarable.rb +136 -0
- data/lib/poetry/core/preview/template.rb +186 -0
- data/lib/poetry/core/recipe_items.rb +82 -0
- data/lib/poetry/core/registry.rb +363 -0
- data/lib/poetry/core/registry_address.rb +115 -0
- data/lib/poetry/core/registry_client.rb +217 -0
- data/lib/poetry/core/registry_installer.rb +234 -0
- data/lib/poetry/core/registry_items.rb +184 -0
- data/lib/poetry/core/skill_text.rb +288 -0
- data/lib/poetry/core/stable_id.rb +110 -0
- data/lib/poetry/core/stimulus/builder.rb +395 -0
- data/lib/poetry/core/stimulus/declarations.rb +444 -0
- data/lib/poetry/core/stimulus/manifest.rb +68 -0
- data/lib/poetry/core/stimulus/merger.rb +131 -0
- data/lib/poetry/core/stimulus_contract.rb +263 -0
- data/lib/poetry/core/tag_helper.rb +10 -0
- data/lib/poetry/core/template_compile.rb +74 -0
- data/lib/poetry/core/token_import.rb +317 -0
- data/lib/poetry/core/tokens/color.rb +215 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
- data/lib/poetry/core/tokens/generator.rb +211 -0
- data/lib/poetry/core/tokens.rb +117 -0
- data/lib/poetry/core/version.rb +9 -0
- data/lib/poetry/core.rb +52 -0
- data/lib/poetry-core.rb +5 -0
- data/tokens/aliases.css +145 -0
- data/tokens/tailwind-theme.css +47 -0
- data/tokens/tokens.css +78 -0
- data/tokens/tokens.dtcg.json +779 -0
- data/vendor/floating-ui/LICENSE +20 -0
- data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
- data/vendor/shadcn-tailwind/LICENSE +21 -0
- data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
- data/vendor/shadcn-tailwind/tailwind.css +629 -0
- data/vendor/tw-animate-css/LICENSE +21 -0
- data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
- data/vendor/tw-animate-css/tw-animate.css +1 -0
- metadata +267 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Core
|
|
5
|
+
# Generates the component-usage Claude Code skill from the registry -
|
|
6
|
+
# the same never-hand-maintained discipline as LlmsText, one delivery
|
|
7
|
+
# surface over: a lean SKILL.md menu (guardrails + family index) over
|
|
8
|
+
# references/ files an agent loads selectively, so a 65-component
|
|
9
|
+
# catalog stays inside the context budget.
|
|
10
|
+
# The family partition itself belongs to the UI gem (it knows its
|
|
11
|
+
# roster); this class only formats.
|
|
12
|
+
#
|
|
13
|
+
# @example The installable skill file map
|
|
14
|
+
# Poetry::Core::SkillText.new(registry: registry, families: families).files
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
class SkillText < LlmsText
|
|
18
|
+
def initialize(registry:, families:, charts_registry: nil)
|
|
19
|
+
super(registry: registry)
|
|
20
|
+
@families = families
|
|
21
|
+
@charts_registry = charts_registry
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The installable file map, paths relative to .claude/skills/poetry/.
|
|
25
|
+
def files
|
|
26
|
+
files = { "SKILL.md" => skill_md }
|
|
27
|
+
@families.each_key { |family| files["references/#{family}.md"] = family_reference(family) }
|
|
28
|
+
files["references/blocks.md"] = blocks_reference
|
|
29
|
+
files["references/deciding.md"] = self.class.deciding_reference
|
|
30
|
+
files["references/charts.md"] = charts_reference if @charts_registry
|
|
31
|
+
files
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Contract sections for every entry, llms-full format - public here
|
|
35
|
+
# (unlike LlmsText's internals) so a charts-registry instance can
|
|
36
|
+
# lend its sections to the main skill's charts reference.
|
|
37
|
+
def sections(paths = nil)
|
|
38
|
+
entries = paths ? @registry.entries.slice(*paths) : @registry.entries
|
|
39
|
+
entries.map { |path, entry| component_section(path, entry) }.join("\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The component decision tree: curated head-to-heads keyed on the
|
|
43
|
+
# INTERACTION MODEL. The
|
|
44
|
+
# roster facts it names are gate-checked (doc-prose scans installed
|
|
45
|
+
# skill prose), so keep every claim registry-true. A PUBLIC class
|
|
46
|
+
# method: the boot-free MCP agent serves it too (the guidance tool),
|
|
47
|
+
# without a registry in hand.
|
|
48
|
+
def self.deciding_reference
|
|
49
|
+
<<~MD
|
|
50
|
+
# deciding - which component
|
|
51
|
+
|
|
52
|
+
Match the INTERACTION MODEL first - what the user does - and the
|
|
53
|
+
visual treatment second. The look is the theme's job; the
|
|
54
|
+
component's job is behavior. When a whole screen is the brief,
|
|
55
|
+
the `compose` MCP tool routes to a vetted block BEFORE any of
|
|
56
|
+
this (see `references/blocks.md`).
|
|
57
|
+
|
|
58
|
+
## Options are VALUES vs options DO things
|
|
59
|
+
|
|
60
|
+
- Choosing writes a form value: Select (closed pick),
|
|
61
|
+
NativeSelect (zero-JS forms), Combobox (filter to pick),
|
|
62
|
+
Combobox `multiple:` (pick several - chips), RadioGroup (few
|
|
63
|
+
always-visible exclusive choices), ToggleGroup `type: :single`
|
|
64
|
+
(exclusive UI state, no form machinery).
|
|
65
|
+
- Choosing runs an action: DropdownMenu (per-item actions),
|
|
66
|
+
ContextMenu (right-click/long-press), Menubar (app-wide command
|
|
67
|
+
strip), Command (searchable palette).
|
|
68
|
+
- The rule: if the choice submits, it is never a menu; if it
|
|
69
|
+
navigates or mutates, it is never a select.
|
|
70
|
+
|
|
71
|
+
## Overlays
|
|
72
|
+
|
|
73
|
+
- Dialog: interrupt for a task; focus trapped; explicit close.
|
|
74
|
+
- AlertDialog: confirm a destructive or irreversible act - no
|
|
75
|
+
light dismiss, the cancel action is the default focus.
|
|
76
|
+
- Sheet: a side panel for secondary work while the page stays
|
|
77
|
+
visible; Drawer: the bottom-edge mobile-first surface.
|
|
78
|
+
- Popover: a light-dismiss micro-surface anchored to its trigger.
|
|
79
|
+
- HoverCard: a hover PREVIEW - never interactive controls.
|
|
80
|
+
- Tooltip: one line of labeling; never actions, never required
|
|
81
|
+
information.
|
|
82
|
+
|
|
83
|
+
## Chips, toggles, badges
|
|
84
|
+
|
|
85
|
+
- Exists until removed (recipients, filters): TagGroup.
|
|
86
|
+
- On/off UI state: Toggle; exclusive set: ToggleGroup.
|
|
87
|
+
- Picked from options: Combobox `multiple:` (its chips remove
|
|
88
|
+
back into the option list; TagGroup chips are just gone).
|
|
89
|
+
- Static status: Badge - never clickable.
|
|
90
|
+
|
|
91
|
+
## Quantity, progress, waiting
|
|
92
|
+
|
|
93
|
+
- A quantity within a known range (disk, seats, strength): Meter.
|
|
94
|
+
- An operation completing over time: Progress (determinate only).
|
|
95
|
+
- Unknown duration: Spinner; structure-shaped waits: Skeleton;
|
|
96
|
+
Turbo-loaded regions: Deferred.
|
|
97
|
+
|
|
98
|
+
## Text and value entry
|
|
99
|
+
|
|
100
|
+
- One line: Input; multi-line: Textarea; search: SearchField
|
|
101
|
+
(Escape clears, the clear affordance rides it).
|
|
102
|
+
- Fixed format: Input `mask:`; numbers: NumberField; one-time
|
|
103
|
+
codes: InputOtp.
|
|
104
|
+
- Dates and times: DateField / TimeField (segmented editing);
|
|
105
|
+
DatePicker when a calendar aids the pick; Calendar alone for
|
|
106
|
+
in-page selection; Slider / range for magnitudes.
|
|
107
|
+
- File upload: FileInput (`variant: :dropzone` when dragging is
|
|
108
|
+
the point).
|
|
109
|
+
|
|
110
|
+
## Structure and records
|
|
111
|
+
|
|
112
|
+
- Tabular records with sort/filter/page: DataTable
|
|
113
|
+
(server-driven URL state); plain semantics: Table.
|
|
114
|
+
- Label:value facts on a detail page: MetadataList; one KPI:
|
|
115
|
+
Stat; grouped content: Card; freeform rows: Item.
|
|
116
|
+
- Hierarchy that expands and collapses: Tree; app navigation:
|
|
117
|
+
Sidebar (shell) / NavigationMenu (site) / Tabs (views of one
|
|
118
|
+
thing); progressive disclosure: Accordion (a one-off:
|
|
119
|
+
Collapsible).
|
|
120
|
+
- Grouped controls in ONE Tab stop: Toolbar; visually fused
|
|
121
|
+
buttons: ButtonGroup.
|
|
122
|
+
|
|
123
|
+
## When two still fit
|
|
124
|
+
|
|
125
|
+
Prefer the narrower component (Stat over a hand-built Card;
|
|
126
|
+
SearchField over Input-plus-button), and prefer the one whose
|
|
127
|
+
KEYBOARD contract matches what the user expects to press. If the
|
|
128
|
+
answer still is not obvious, the block catalog probably already
|
|
129
|
+
composed it - check `references/blocks.md` before building.
|
|
130
|
+
MD
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def skill_md
|
|
136
|
+
<<~MD
|
|
137
|
+
---
|
|
138
|
+
name: poetry
|
|
139
|
+
description: >-
|
|
140
|
+
Build Rails views with the poetry component library: helper
|
|
141
|
+
contracts, options, slots, blocks, and the check workflow. Use
|
|
142
|
+
whenever writing or editing ERB/UI in an app that has poetry
|
|
143
|
+
installed.
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
# poetry - component usage
|
|
147
|
+
|
|
148
|
+
Generated from the poetry registry (#{census}). After updating
|
|
149
|
+
poetry gems, regenerate with `bin/rails g poetry:skill`.
|
|
150
|
+
|
|
151
|
+
## Guardrails
|
|
152
|
+
|
|
153
|
+
- FIRST MOVE, for every brief: call the poetry MCP `compose` tool
|
|
154
|
+
with the task text, before writing any ERB. It routes you to the
|
|
155
|
+
matching vetted block (source included, adapt in place - the
|
|
156
|
+
known winning path for screens) or to the right components for
|
|
157
|
+
single-component work. No MCP? Open `references/blocks.md` and
|
|
158
|
+
`bin/rails g poetry:block --list`. Composing a screen from
|
|
159
|
+
scratch when a block matched is the known losing path.
|
|
160
|
+
- Compose with the `poetry_<name>` helpers; never hand-write `cn-*`
|
|
161
|
+
classes, raw hex/oklch colors, or off-scale arbitrary values -
|
|
162
|
+
tokens and variants carry the design.
|
|
163
|
+
- Options are keywords; content is the block. Helpers take at most
|
|
164
|
+
the positional arguments their contract lists - most take none.
|
|
165
|
+
- A typed slot renders another component: the call takes THAT
|
|
166
|
+
component's props, never a render block.
|
|
167
|
+
- Icon names are kebab-case symbols: `:"circle-check"`, never
|
|
168
|
+
`:circle_check`.
|
|
169
|
+
- Status reads as a set: one badge treatment family per surface -
|
|
170
|
+
never mix solid (default/destructive) and soft
|
|
171
|
+
(success/warning/info) pills in one table.
|
|
172
|
+
- Page framing: a section that IS the page's subject keeps its
|
|
173
|
+
container and breathing room (`mx-auto max-w-* p-6`); a bare
|
|
174
|
+
component at the viewport origin reads cramped. Drop the wrapper
|
|
175
|
+
when composing into an already-padded frame.
|
|
176
|
+
- One visual theme per app (chosen at install); components read
|
|
177
|
+
tokens, never restate them.
|
|
178
|
+
- Browser agents (WebMCP): opt a rendered component into the user's
|
|
179
|
+
own agent with `webmcp: "name"` on the call - only components that
|
|
180
|
+
declare tools (Combobox, Dialog, Sheet, Drawer, Tabs; `describe_component`
|
|
181
|
+
at `full` lists them); a form becomes a tool with
|
|
182
|
+
`poetry_webmcp_form(tool: { name:, description: })` (autosubmit is
|
|
183
|
+
GET-only). Needs the poetry-agent gem; check gates the opt-ins.
|
|
184
|
+
- Check comes LAST: run `bin/rails poetry:check` (or the poetry MCP
|
|
185
|
+
`check` tool - instant, no app boot) as the FINAL action, after
|
|
186
|
+
your last edit. An edit made after your last check is unverified
|
|
187
|
+
markup - re-run check before finishing, every time.
|
|
188
|
+
|
|
189
|
+
## Find your component
|
|
190
|
+
|
|
191
|
+
Not sure WHICH component the job calls for? Open
|
|
192
|
+
`references/deciding.md` first - the decision tree matches the
|
|
193
|
+
INTERACTION MODEL (what the user does), never the visual look.
|
|
194
|
+
|
|
195
|
+
Load the reference for the family you are composing in - each file
|
|
196
|
+
carries the full contracts (options, variants, slots, wiring, RULE
|
|
197
|
+
lines) for its components:
|
|
198
|
+
|
|
199
|
+
#{family_index}
|
|
200
|
+
|
|
201
|
+
## Composing a page? Load poetry-design
|
|
202
|
+
|
|
203
|
+
Building or restyling a full page, screen, or dashboard - not a
|
|
204
|
+
lone component? Load the `poetry-design` skill BEFORE composing:
|
|
205
|
+
theme fit, page macrostructure, hierarchy, status color, and the
|
|
206
|
+
finishing audit live there. Component contracts alone do not make
|
|
207
|
+
a composed page - and neither does guidance: start the page from
|
|
208
|
+
`compose`'s block match and adapt, don't rebuild its advice from
|
|
209
|
+
a blank file.
|
|
210
|
+
|
|
211
|
+
## Authoring a component? Load poetry-component
|
|
212
|
+
|
|
213
|
+
Building a component of your own - one this catalog doesn't
|
|
214
|
+
cover? Load the `poetry-component` skill BEFORE writing the
|
|
215
|
+
class: the canonical anatomy (section order), the documentation
|
|
216
|
+
standard, and the audit checklist live there.
|
|
217
|
+
MD
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def census
|
|
221
|
+
parts = ["#{@registry.entries.size} components"]
|
|
222
|
+
parts << "#{@charts_registry.entries.size} chart components" if @charts_registry
|
|
223
|
+
blocks = @registry.blocks
|
|
224
|
+
parts << "#{blocks.size} blocks" if blocks&.any?
|
|
225
|
+
parts.join(" + ")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def family_index
|
|
229
|
+
lines = @families.map do |family, members|
|
|
230
|
+
"- **#{family}** (`references/#{family}.md`): #{members.join(", ")}"
|
|
231
|
+
end
|
|
232
|
+
lines << "- **blocks** (`references/blocks.md`): #{(@registry.blocks || {}).keys.join(", ")}"
|
|
233
|
+
lines << "- **charts** (`references/charts.md`): #{chart_names.join(", ")}" if @charts_registry
|
|
234
|
+
lines.join("\n")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def chart_names
|
|
238
|
+
@charts_registry.entries.keys.map { |path| path.split("/").drop(2).join("_") }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def family_reference(family)
|
|
242
|
+
members = @families.fetch(family)
|
|
243
|
+
paths = @registry.entries.keys.select do |path|
|
|
244
|
+
members.include?(path.split("/").drop(2).join("_"))
|
|
245
|
+
end
|
|
246
|
+
<<~MD
|
|
247
|
+
# poetry #{family} components
|
|
248
|
+
|
|
249
|
+
Contracts generated from the registry. `RULE` lines are constraints,
|
|
250
|
+
not suggestions. Options are keywords; content is the block.
|
|
251
|
+
|
|
252
|
+
#{sections(paths)}
|
|
253
|
+
MD
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The blocks reference inlines every block's full source - the block
|
|
257
|
+
# WITH source is the load-bearing agent path.
|
|
258
|
+
def blocks_reference
|
|
259
|
+
<<~MD
|
|
260
|
+
# poetry blocks - vetted composed screens
|
|
261
|
+
|
|
262
|
+
Blocks are the DEFAULT starting point for a screen, not a
|
|
263
|
+
fallback: the MCP `compose` tool routes a brief to the right one
|
|
264
|
+
automatically (call it first); this file carries the same catalog
|
|
265
|
+
with full source. `bin/rails g poetry:block <name>` copies one
|
|
266
|
+
into app/views/blocks/ as source the app owns. Blocks carry the
|
|
267
|
+
composed patterns - containment, status color-coding, page
|
|
268
|
+
furniture, realistic content - so a screen starts composed, not
|
|
269
|
+
blank. The sample content is meant to be replaced.
|
|
270
|
+
#{blocks_full}
|
|
271
|
+
MD
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def charts_reference
|
|
275
|
+
charts = self.class.new(registry: @charts_registry, families: {})
|
|
276
|
+
<<~MD
|
|
277
|
+
# poetry chart components
|
|
278
|
+
|
|
279
|
+
Contracts generated from the charts registry. Chart data is
|
|
280
|
+
server-rendered; the `poetry_chart(type, ...)` shorthand takes the
|
|
281
|
+
chart type as its one positional argument.
|
|
282
|
+
|
|
283
|
+
#{charts.sections}
|
|
284
|
+
MD
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_view/record_identifier"
|
|
4
|
+
require "digest/md5"
|
|
5
|
+
require "random/formatter"
|
|
6
|
+
|
|
7
|
+
module Poetry
|
|
8
|
+
module Core
|
|
9
|
+
# Semantic identity for component DOM ids: a
|
|
10
|
+
# caller-supplied key: becomes a component-namespaced, render-stable
|
|
11
|
+
# token, so Turbo morph pairs the same logical component across
|
|
12
|
+
# renders and cached fragments stay composable.
|
|
13
|
+
#
|
|
14
|
+
# @example A render-stable id derived from a record
|
|
15
|
+
# Poetry::Core::StableId.key_token(article) # => "article_42"
|
|
16
|
+
#
|
|
17
|
+
# Derivation is deliberately dom_id-first, never slug-sniffing:
|
|
18
|
+
# records go through ActionView::RecordIdentifier.dom_id, which
|
|
19
|
+
# respects a host's `def to_key = [slug]` override - the one lever
|
|
20
|
+
# that keeps key:, turbo_frame_tag, and Turbo Stream broadcast
|
|
21
|
+
# targets emitting the SAME identity everywhere (turbo-rails calls
|
|
22
|
+
# RecordIdentifier internally). Plain values parameterize. Slugs
|
|
23
|
+
# read directly would be mutable and recyclable (a freed slug
|
|
24
|
+
# claimed by another record re-creates false identity), and poetry
|
|
25
|
+
# preferring them while the host's own dom_id stays pk-based would
|
|
26
|
+
# split one app into two id vocabularies.
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
29
|
+
module StableId
|
|
30
|
+
# The thread-local sequence generator (the OPT-IN mode, default
|
|
31
|
+
# off). Same page -> same seed -> identical id sequence across
|
|
32
|
+
# renders, which is what byte-stable content pages need for Turbo
|
|
33
|
+
# morph and body-hash ETags. The hazards that keep it opt-in are
|
|
34
|
+
# documented where the mode is: same-path turbo-frames restart the
|
|
35
|
+
# sequence and collide deterministically, and reordered same-type
|
|
36
|
+
# collections get POSITIONAL false identity (state on the wrong
|
|
37
|
+
# record) - keyed identity (key:) is the general answer; this mode
|
|
38
|
+
# exists for static/content pages a host judges safe.
|
|
39
|
+
SEQUENCE_KEY = :poetry_stable_id_sequence
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# Deterministic ids inside the block: with_seed is honored
|
|
44
|
+
# REGARDLESS of stable_id_mode (an explicit call is explicit
|
|
45
|
+
# intent - it is also the test API). The around_action is what the
|
|
46
|
+
# mode gates.
|
|
47
|
+
def with_seed(seed)
|
|
48
|
+
previous = Thread.current[SEQUENCE_KEY]
|
|
49
|
+
Thread.current[SEQUENCE_KEY] = sequence_for(seed)
|
|
50
|
+
yield
|
|
51
|
+
ensure
|
|
52
|
+
Thread.current[SEQUENCE_KEY] = previous
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The next deterministic token, or nil when no sequence is armed
|
|
56
|
+
# (callers fall through to the random rung).
|
|
57
|
+
def next_sequence_token
|
|
58
|
+
Thread.current[SEQUENCE_KEY]&.hex(8)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Random::Formatter over a digest-seeded PRNG:
|
|
62
|
+
# hex(8) matches the random fallback's shape exactly, so the mode
|
|
63
|
+
# is invisible in the DOM.
|
|
64
|
+
def sequence_for(seed)
|
|
65
|
+
Random.new(Digest::MD5.hexdigest(seed.to_s).to_i(16))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The opt-in request seeding (engine-installed on every controller,
|
|
69
|
+
# inert unless stable_id_mode == :sequence): seeds the thread-local
|
|
70
|
+
# sequence from the configured lambda (default: request.path -
|
|
71
|
+
# matching Turbo's own page-refresh pathname test) around each
|
|
72
|
+
# action, always restoring after.
|
|
73
|
+
module Controller
|
|
74
|
+
extend ActiveSupport::Concern
|
|
75
|
+
|
|
76
|
+
included do
|
|
77
|
+
around_action :poetry_stable_id_sequence
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def poetry_stable_id_sequence(&)
|
|
83
|
+
config = Poetry::Core::Config.current
|
|
84
|
+
return yield unless config.stable_id_mode == :sequence
|
|
85
|
+
|
|
86
|
+
Poetry::Core::StableId.with_seed(config.stable_id_seed.call(request), &)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The stable token for a key:, or nil when no usable token exists
|
|
91
|
+
# (callers fall back to random - fail toward correctness).
|
|
92
|
+
# Records (anything dom_id can address) -> dom_id; everything else
|
|
93
|
+
# -> to_s.parameterize. New records derive "new_<model>" - two of
|
|
94
|
+
# those on one page collide, so repeated new-record forms need
|
|
95
|
+
# explicit keys (documented).
|
|
96
|
+
#
|
|
97
|
+
# @param key [Object, nil] a record, a plain value, or nil
|
|
98
|
+
# @return [String, nil] the render-stable token, or nil
|
|
99
|
+
def key_token(key)
|
|
100
|
+
return nil if key.nil?
|
|
101
|
+
|
|
102
|
+
if key.respond_to?(:to_model) || key.respond_to?(:model_name)
|
|
103
|
+
ActionView::RecordIdentifier.dom_id(key)
|
|
104
|
+
else
|
|
105
|
+
key.to_s.parameterize.presence
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|