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,439 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import {
|
|
3
|
+
arrow,
|
|
4
|
+
autoUpdate,
|
|
5
|
+
computePosition,
|
|
6
|
+
flip,
|
|
7
|
+
hide,
|
|
8
|
+
limitShift,
|
|
9
|
+
offset,
|
|
10
|
+
shift,
|
|
11
|
+
size
|
|
12
|
+
} from "@poetry/controllers/vendor/floating_ui_dom"
|
|
13
|
+
|
|
14
|
+
// The anchored-positioning primitive: one
|
|
15
|
+
// controller over the VENDORED @floating-ui/dom. A fixed middleware
|
|
16
|
+
// pipeline - offset -> shift -> flip (both gated on avoidCollisions)
|
|
17
|
+
// -> size -> arrow (when an arrow target exists) -> hide - and a fixed
|
|
18
|
+
// output contract: data-side / data-align mirror the RESOLVED placement
|
|
19
|
+
// (after flip), and the content gets the sizing CSS vars
|
|
20
|
+
// (--transform-origin / -available-width / -available-height /
|
|
21
|
+
// -anchor-width / -anchor-height) so ported Tailwind classes like
|
|
22
|
+
// data-[side=top]:slide-in-from-bottom-2 and
|
|
23
|
+
// max-h-[var(--available-height)] work unchanged.
|
|
24
|
+
//
|
|
25
|
+
// The Arrow stays a markup primitive: any [data-slot=popper-arrow]
|
|
26
|
+
// marked as the arrow target is positioned + given data-side here; the SVG
|
|
27
|
+
// itself is poetry-ui's business.
|
|
28
|
+
//
|
|
29
|
+
// VIRTUAL-ANCHOR mode (the ContextMenu contract): a non-empty anchorPoint
|
|
30
|
+
// value - canonically the data-poetry--core--popper-anchor-point="x,y"
|
|
31
|
+
// attribute on the controller root (the DOM is the store) - floats against a
|
|
32
|
+
// floating-ui VirtualElement: a zero-size rect at (x,y) client coords.
|
|
33
|
+
// The resolved ELEMENT anchor rides along as
|
|
34
|
+
// contextElement so autoUpdate still tracks ancestor scroll/resize.
|
|
35
|
+
// setAnchor(x, y) is the ergonomics wrapper ContextMenu calls from its
|
|
36
|
+
// contextmenu / long-press handlers; clearing the value returns to plain
|
|
37
|
+
// element anchoring.
|
|
38
|
+
//
|
|
39
|
+
// Consumers: Popover, Dropdown/Context Menu, Tooltip, HoverCard, Select,
|
|
40
|
+
// Combobox, Menubar.
|
|
41
|
+
const SIDES = ["top", "right", "bottom", "left"]
|
|
42
|
+
const ALIGNS = ["start", "center", "end"]
|
|
43
|
+
|
|
44
|
+
// The side the arrow sits on / the transform-origin edge is the OPPOSITE of
|
|
45
|
+
// where the content was placed.
|
|
46
|
+
const OPPOSITE_SIDE = { top: "bottom", right: "left", bottom: "top", left: "right" }
|
|
47
|
+
|
|
48
|
+
// The arrow rotation table: the arrow component points down by default;
|
|
49
|
+
// these transforms swing it toward the anchor for each resolved side.
|
|
50
|
+
const ARROW_TRANSFORM = {
|
|
51
|
+
top: "translateY(100%)",
|
|
52
|
+
right: "translateY(50%) rotate(90deg) translateX(-50%)",
|
|
53
|
+
bottom: "rotate(180deg)",
|
|
54
|
+
left: "translateY(50%) rotate(-90deg) translateX(50%)"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ARROW_TRANSFORM_ORIGIN = { top: "", right: "0 0", bottom: "center 0", left: "100% 0" }
|
|
58
|
+
|
|
59
|
+
const NO_ARROW_ALIGN_ORIGIN = { start: "0%", center: "50%", end: "100%" }
|
|
60
|
+
|
|
61
|
+
export default class PopperController extends Controller {
|
|
62
|
+
static targets = ["anchor", "content", "arrow"]
|
|
63
|
+
static values = {
|
|
64
|
+
// Anchor fallback chain: anchor target -> this selector -> the element's
|
|
65
|
+
// previousElementSibling (the natural trigger-then-content markup shape).
|
|
66
|
+
anchor: { type: String, default: "" },
|
|
67
|
+
// "x,y" client coords -> virtual-anchor mode (see the header). Empty =
|
|
68
|
+
// element anchoring. Reactive: changing it re-arms autoUpdate.
|
|
69
|
+
anchorPoint: { type: String, default: "" },
|
|
70
|
+
side: { type: String, default: "bottom" }, // top | right | bottom | left
|
|
71
|
+
align: { type: String, default: "center" }, // start | center | end
|
|
72
|
+
sideOffset: { type: Number, default: 0 },
|
|
73
|
+
alignOffset: { type: Number, default: 0 },
|
|
74
|
+
avoidCollisions: { type: Boolean, default: true },
|
|
75
|
+
strategy: { type: String, default: "fixed" } // fixed | absolute
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#stopAutoUpdate = null
|
|
79
|
+
#anchorElement = null
|
|
80
|
+
#contentElement = null
|
|
81
|
+
#arrowElement = null
|
|
82
|
+
#generation = 0
|
|
83
|
+
#connected = false
|
|
84
|
+
#hidPointerEvents = false
|
|
85
|
+
#pointerEventsBeforeHide = ""
|
|
86
|
+
|
|
87
|
+
/** Arms autoUpdate (which fires one immediate position pass). */
|
|
88
|
+
connect() {
|
|
89
|
+
this.#connected = true
|
|
90
|
+
this.#start()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Disarms, stranding any in-flight update (the generation bump). */
|
|
94
|
+
disconnect() {
|
|
95
|
+
this.#connected = false
|
|
96
|
+
this.#generation += 1 // strand any in-flight #update await
|
|
97
|
+
this.#stop()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Stimulus value callback - reactive re-anchor: switching between
|
|
102
|
+
* element and virtual anchoring (or moving the point) re-arms
|
|
103
|
+
* autoUpdate against the new reference. Fires during initialization
|
|
104
|
+
* too, before connect - the connected guard skips that first call.
|
|
105
|
+
*/
|
|
106
|
+
anchorPointValueChanged() {
|
|
107
|
+
if (!this.#connected) return
|
|
108
|
+
|
|
109
|
+
this.#stop()
|
|
110
|
+
this.#start()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stimulus value callback - reactive strategy (the portal seam): a
|
|
115
|
+
* consumer flips fixed <-> absolute when it portals/restores content;
|
|
116
|
+
* re-arming repositions in the new coordinate space immediately.
|
|
117
|
+
*/
|
|
118
|
+
strategyValueChanged() {
|
|
119
|
+
if (!this.#connected) return
|
|
120
|
+
|
|
121
|
+
this.#stop()
|
|
122
|
+
this.#start()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Manual re-run for consumers whose geometry changed outside
|
|
127
|
+
* autoUpdate's sensors (e.g. content swapped by a Turbo Stream).
|
|
128
|
+
*
|
|
129
|
+
* @returns {Promise<void>} the update promise, so callers can await
|
|
130
|
+
* settlement
|
|
131
|
+
*/
|
|
132
|
+
reposition() {
|
|
133
|
+
return this.#update()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The ContextMenu entry point: stores the pointer coords (the ATTRIBUTE
|
|
138
|
+
* is canonical - inspectable, server-settable) and repositions
|
|
139
|
+
* immediately; the value-changed callback then re-arms autoUpdate's
|
|
140
|
+
* tracking.
|
|
141
|
+
*
|
|
142
|
+
* @param {number} x - client coords
|
|
143
|
+
* @param {number} y
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
setAnchor(x, y) {
|
|
147
|
+
this.anchorPointValue = `${x},${y}`
|
|
148
|
+
|
|
149
|
+
return this.reposition()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The NavigationMenu viewport entry point: floats against a
|
|
154
|
+
* caller-supplied element (the active trigger) instead of a
|
|
155
|
+
* target/selector. Re-arms autoUpdate so the new reference's ancestors
|
|
156
|
+
* are tracked, then repositions - the positioner's inset transition
|
|
157
|
+
* turns that write into the position morph.
|
|
158
|
+
*
|
|
159
|
+
* @param {Element} element
|
|
160
|
+
* @returns {Promise<void>}
|
|
161
|
+
*/
|
|
162
|
+
setAnchorElement(element) {
|
|
163
|
+
this.#anchorElement = element
|
|
164
|
+
this.#stop()
|
|
165
|
+
this.#start()
|
|
166
|
+
|
|
167
|
+
return this.reposition()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- positioning ---
|
|
171
|
+
|
|
172
|
+
#start() {
|
|
173
|
+
const reference = this.#reference()
|
|
174
|
+
const content = this.#content()
|
|
175
|
+
|
|
176
|
+
if (!reference || !content) return
|
|
177
|
+
|
|
178
|
+
// autoUpdate re-runs positioning on scroll / resize / anchor movement and
|
|
179
|
+
// fires once immediately, so start needs no separate first update.
|
|
180
|
+
this.#stopAutoUpdate = autoUpdate(reference, content, () => this.#update())
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#stop() {
|
|
184
|
+
this.#stopAutoUpdate?.()
|
|
185
|
+
this.#stopAutoUpdate = null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async #update() {
|
|
189
|
+
const anchor = this.#reference()
|
|
190
|
+
const content = this.#content()
|
|
191
|
+
|
|
192
|
+
if (!anchor || !content) return
|
|
193
|
+
// Select's alignItemWithTrigger mode fixed-positions the
|
|
194
|
+
// content itself; while the attribute is on, popper must not fight it.
|
|
195
|
+
if (content.hasAttribute("data-align-item-with-trigger")) return
|
|
196
|
+
|
|
197
|
+
const arrowElement = this.#arrow()
|
|
198
|
+
const generation = this.#generation
|
|
199
|
+
|
|
200
|
+
const { x, y, placement, strategy, middlewareData } = await computePosition(anchor, content, {
|
|
201
|
+
strategy: this.strategyValue,
|
|
202
|
+
placement: this.#placement(),
|
|
203
|
+
middleware: this.#middleware(arrowElement)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Disconnected (or superseded) while awaiting: never write to a dead node.
|
|
207
|
+
if (generation !== this.#generation) return
|
|
208
|
+
// Re-check after the await: aligned mode can engage DURING
|
|
209
|
+
// computePosition (open -> update starts -> select fixed-positions
|
|
210
|
+
// the content -> compute resolves), and a stale write here clobbers
|
|
211
|
+
// the fixed placement with absolute popper coordinates.
|
|
212
|
+
if (content.hasAttribute("data-align-item-with-trigger")) return
|
|
213
|
+
|
|
214
|
+
const [side, align = "center"] = placement.split("-")
|
|
215
|
+
|
|
216
|
+
content.style.position = strategy
|
|
217
|
+
content.style.left = `${x}px`
|
|
218
|
+
content.style.top = `${y}px`
|
|
219
|
+
|
|
220
|
+
// The RESOLVED side/align (after flip) - what the ported data-[side=...]
|
|
221
|
+
// Tailwind variants key off.
|
|
222
|
+
content.setAttribute("data-side", side)
|
|
223
|
+
content.setAttribute("data-align", align)
|
|
224
|
+
|
|
225
|
+
content.style.setProperty(
|
|
226
|
+
"--transform-origin",
|
|
227
|
+
this.#transformOrigin({ side, align, content, arrowElement, middlewareData })
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if (middlewareData.hide) {
|
|
231
|
+
// Never write the hide verdict onto CLOSED content: a closed-era
|
|
232
|
+
// pass measures a display:none box (garbage geometry), and its
|
|
233
|
+
// stale visibility:hidden lingers into the open MICROTASK, where
|
|
234
|
+
// Chrome silently refuses focus-scope's mount focus (the popover
|
|
235
|
+
// first-open race - pre-existing, surfaced by the portal
|
|
236
|
+
// proofs). While content is hidden the verdict resolves to
|
|
237
|
+
// visible, which routes through the clear/restore branch below so
|
|
238
|
+
// every open starts clean.
|
|
239
|
+
const hidden = !content.hidden && Boolean(middlewareData.hide.referenceHidden)
|
|
240
|
+
|
|
241
|
+
content.style.visibility = hidden ? "hidden" : ""
|
|
242
|
+
|
|
243
|
+
// Save/restore, never clear: the dismissable scrim opts this SAME
|
|
244
|
+
// element back in (pointer-events: auto) while body is none -
|
|
245
|
+
// blanking it on every position pass left modal popovers click-dead.
|
|
246
|
+
// Found by the real-pointer tester proofs (the first real-pointer gate;
|
|
247
|
+
// dommy dispatches synthetically, so pointer-events was invisible
|
|
248
|
+
// to it).
|
|
249
|
+
if (hidden && !this.#hidPointerEvents) {
|
|
250
|
+
this.#hidPointerEvents = true
|
|
251
|
+
this.#pointerEventsBeforeHide = content.style.pointerEvents
|
|
252
|
+
content.style.pointerEvents = "none"
|
|
253
|
+
} else if (!hidden && this.#hidPointerEvents) {
|
|
254
|
+
this.#hidPointerEvents = false
|
|
255
|
+
content.style.pointerEvents = this.#pointerEventsBeforeHide
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (arrowElement) this.#positionArrow(arrowElement, side, middlewareData)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The pipeline order is contractual. offset folds the arrow's
|
|
263
|
+
// height into the main axis, so the gap accounts for
|
|
264
|
+
// the arrow between anchor and content.
|
|
265
|
+
#middleware(arrowElement) {
|
|
266
|
+
const { height: arrowHeight } = this.#arrowSize(arrowElement)
|
|
267
|
+
const middleware = [
|
|
268
|
+
offset({ mainAxis: this.sideOffsetValue + arrowHeight, alignmentAxis: this.alignOffsetValue })
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
if (this.avoidCollisionsValue) {
|
|
272
|
+
middleware.push(shift({ mainAxis: true, crossAxis: false, limiter: limitShift() }))
|
|
273
|
+
middleware.push(flip())
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
middleware.push(
|
|
277
|
+
size({
|
|
278
|
+
apply: ({ elements, rects, availableWidth, availableHeight }) => {
|
|
279
|
+
// The sizing vars, written from inside the pipeline so
|
|
280
|
+
// they reflect post-flip geometry.
|
|
281
|
+
const style = elements.floating.style
|
|
282
|
+
|
|
283
|
+
style.setProperty("--available-width", `${availableWidth}px`)
|
|
284
|
+
style.setProperty("--available-height", `${availableHeight}px`)
|
|
285
|
+
style.setProperty("--anchor-width", `${rects.reference.width}px`)
|
|
286
|
+
style.setProperty("--anchor-height", `${rects.reference.height}px`)
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if (arrowElement) middleware.push(arrow({ element: arrowElement }))
|
|
292
|
+
|
|
293
|
+
middleware.push(hide({ strategy: "referenceHidden" }))
|
|
294
|
+
|
|
295
|
+
return middleware
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#placement() {
|
|
299
|
+
const side = SIDES.includes(this.sideValue) ? this.sideValue : "bottom"
|
|
300
|
+
const align = ALIGNS.includes(this.alignValue) ? this.alignValue : "center"
|
|
301
|
+
|
|
302
|
+
return align === "center" ? side : `${side}-${align}`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The transform-origin rule, applied post-compute: the origin sits
|
|
306
|
+
// on the anchor-facing edge, at the arrow's center when there is an arrow,
|
|
307
|
+
// else at the resolved align fraction.
|
|
308
|
+
#transformOrigin({ side, align, content, arrowElement, middlewareData }) {
|
|
309
|
+
const { width: arrowWidth, height: arrowHeight } = this.#arrowSize(arrowElement)
|
|
310
|
+
const alignOrigin = NO_ARROW_ALIGN_ORIGIN[align] ?? "50%"
|
|
311
|
+
const arrowX = (middlewareData.arrow?.x ?? 0) + arrowWidth / 2
|
|
312
|
+
const arrowY = (middlewareData.arrow?.y ?? 0) + arrowHeight / 2
|
|
313
|
+
const across = arrowElement ? `${arrowX}px` : alignOrigin
|
|
314
|
+
const down = arrowElement ? `${arrowY}px` : alignOrigin
|
|
315
|
+
const rect = typeof content.getBoundingClientRect === "function"
|
|
316
|
+
? content.getBoundingClientRect()
|
|
317
|
+
: { width: 0, height: 0 }
|
|
318
|
+
|
|
319
|
+
switch (side) {
|
|
320
|
+
case "top": return `${across} ${rect.height + arrowHeight}px`
|
|
321
|
+
case "bottom": return `${across} ${-arrowHeight}px`
|
|
322
|
+
case "left": return `${rect.width + arrowHeight}px ${down}`
|
|
323
|
+
case "right": return `${-arrowHeight}px ${down}`
|
|
324
|
+
default: return `${alignOrigin} ${alignOrigin}`
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#positionArrow(arrowElement, side, middlewareData) {
|
|
329
|
+
const { x, y } = middlewareData.arrow ?? {}
|
|
330
|
+
|
|
331
|
+
arrowElement.style.position = "absolute"
|
|
332
|
+
// Clear EVERY edge before pinning: after a flip the stale opposite pin
|
|
333
|
+
// survives (top:0 + bottom:0 stretches the arrow to the bubble's full
|
|
334
|
+
// height, and that inflated box feeds the arrow-height gap offset).
|
|
335
|
+
arrowElement.style.top = ""
|
|
336
|
+
arrowElement.style.right = ""
|
|
337
|
+
arrowElement.style.bottom = ""
|
|
338
|
+
arrowElement.style.left = ""
|
|
339
|
+
arrowElement.style.left = x != null ? `${x}px` : ""
|
|
340
|
+
arrowElement.style.top = y != null ? `${y}px` : ""
|
|
341
|
+
// Pin the arrow to the content's anchor-facing edge, then rotate it to
|
|
342
|
+
// point at the anchor (the table above); data-side is ALSO written so
|
|
343
|
+
// ported CSS can restyle per side.
|
|
344
|
+
arrowElement.style[OPPOSITE_SIDE[side]] = "0"
|
|
345
|
+
arrowElement.style.transformOrigin = ARROW_TRANSFORM_ORIGIN[side]
|
|
346
|
+
arrowElement.style.transform = ARROW_TRANSFORM[side]
|
|
347
|
+
arrowElement.setAttribute("data-side", side)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#arrowSize(arrowElement) {
|
|
351
|
+
if (!arrowElement || typeof arrowElement.getBoundingClientRect !== "function") {
|
|
352
|
+
return { width: 0, height: 0 }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const { width, height } = arrowElement.getBoundingClientRect()
|
|
356
|
+
|
|
357
|
+
return { width, height }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// --- elements ---
|
|
361
|
+
|
|
362
|
+
// What computePosition / autoUpdate float against: with a stored anchor
|
|
363
|
+
// point, a VirtualElement whose rect is zero-size at (x,y) - live-read, so
|
|
364
|
+
// a moved point is picked up on every autoUpdate re-read - else the
|
|
365
|
+
// element anchor itself.
|
|
366
|
+
#reference() {
|
|
367
|
+
const anchor = this.#anchor()
|
|
368
|
+
const point = this.#anchorPoint()
|
|
369
|
+
|
|
370
|
+
if (!point) return anchor
|
|
371
|
+
|
|
372
|
+
const reference = {
|
|
373
|
+
getBoundingClientRect: () => {
|
|
374
|
+
const { x, y } = this.#anchorPoint() ?? point
|
|
375
|
+
|
|
376
|
+
return { x, y, width: 0, height: 0, top: y, right: x, bottom: y, left: x }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// The element anchor rides along so autoUpdate observes scroll / resize
|
|
381
|
+
// of the trigger surface's ancestors (the ContextMenu contract).
|
|
382
|
+
if (anchor) reference.contextElement = anchor
|
|
383
|
+
|
|
384
|
+
return reference
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#anchorPoint() {
|
|
388
|
+
const raw = this.anchorPointValue.trim()
|
|
389
|
+
|
|
390
|
+
if (!raw) return null
|
|
391
|
+
|
|
392
|
+
const [x, y] = raw.split(",").map((part) => Number.parseFloat(part))
|
|
393
|
+
|
|
394
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null
|
|
395
|
+
|
|
396
|
+
return { x, y }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#anchor() {
|
|
400
|
+
if (this.#anchorElement) return this.#anchorElement
|
|
401
|
+
if (this.hasAnchorTarget) return this.anchorTarget
|
|
402
|
+
if (this.anchorValue) return document.querySelector(this.anchorValue)
|
|
403
|
+
|
|
404
|
+
return this.element.previousElementSibling
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#content() {
|
|
408
|
+
if (this.hasContentTarget) {
|
|
409
|
+
this.#contentElement = this.contentTarget
|
|
410
|
+
return this.contentTarget
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The portal seam: while content is portaled
|
|
414
|
+
// out, the Stimulus target no longer matches (targets scope to the
|
|
415
|
+
// controller's subtree) - the cached node keeps being positioned.
|
|
416
|
+
// Without the cache the fallback would position the ROOT.
|
|
417
|
+
if (this.#contentElement?.isConnected) return this.#contentElement
|
|
418
|
+
|
|
419
|
+
return this.element
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// The Tier-4 Arrow is markup-only: an explicit arrow target wins, else any
|
|
423
|
+
// [data-slot=popper-arrow] inside the content is positioned.
|
|
424
|
+
#arrow() {
|
|
425
|
+
if (this.hasArrowTarget) {
|
|
426
|
+
this.#arrowElement = this.arrowTarget
|
|
427
|
+
return this.arrowTarget
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// The portal seam, arrow edition (#content's cache, same reason):
|
|
431
|
+
// portaled content takes the arrow out of target scope, and the
|
|
432
|
+
// fallback selector can't see a component-slotted arrow
|
|
433
|
+
// (tooltip-arrow, not popper-arrow) - without the cache the arrow
|
|
434
|
+
// freezes at its first written position forever.
|
|
435
|
+
if (this.#arrowElement?.isConnected) return this.#arrowElement
|
|
436
|
+
|
|
437
|
+
return this.#content()?.querySelector("[data-slot=popper-arrow]") ?? null
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { setState } from "@poetry/controllers/helpers/state"
|
|
3
|
+
|
|
4
|
+
// The Toggle micro-machine (the smallest controller in the suite): flip
|
|
5
|
+
// aria-pressed and mirror the bare data-pressed presence boolean -
|
|
6
|
+
// WRITTEN TOGETHER, never separately. The DOM is the store: no Values, no internal state. This is
|
|
7
|
+
// deliberately NOT poetry--core--checked (different ARIA vocabulary, no
|
|
8
|
+
// input to sync) and not poetry--core--state (that owns aria-expanded
|
|
9
|
+
// disclosure) - three micro-controllers, three vocabularies.
|
|
10
|
+
//
|
|
11
|
+
// No keydown code: Space AND Enter both activate a native button
|
|
12
|
+
// (click) - the platform already implements the keyboard.
|
|
13
|
+
//
|
|
14
|
+
// poetry:toggle:change is CANCELABLE and fires BEFORE the flip renders:
|
|
15
|
+
// preventDefault vetoes it (hosts that must confirm). The pressed value in
|
|
16
|
+
// the detail is the state the toggle is ABOUT to enter.
|
|
17
|
+
export default class PressedController extends Controller {
|
|
18
|
+
// The events this controller dispatches (manifest surface;
|
|
19
|
+
// events_declaration.test.js enforces the list stays honest).
|
|
20
|
+
static events = ["poetry:toggle:change"]
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The click action: flips to the opposite pressed state. No-op while
|
|
24
|
+
* disabled; a listener that vetoes the (cancelable) change event stops
|
|
25
|
+
* the flip.
|
|
26
|
+
*/
|
|
27
|
+
toggle() {
|
|
28
|
+
if (this.#disabled()) return
|
|
29
|
+
|
|
30
|
+
this.#commit(this.element.getAttribute("aria-pressed") !== "true")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// --- the programmatic controllable-state surface (the rollback recipe:
|
|
34
|
+
// hosts revert a failed effect via set(false)) ---
|
|
35
|
+
|
|
36
|
+
/** Programmatically presses the toggle (see set). */
|
|
37
|
+
press() {
|
|
38
|
+
this.set(true)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Programmatically releases the toggle (see set). */
|
|
42
|
+
unpress() {
|
|
43
|
+
this.set(false)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Programmatically writes a pressed state through the same veto path
|
|
48
|
+
* the click takes. No-op while disabled.
|
|
49
|
+
*
|
|
50
|
+
* @param {boolean} pressed
|
|
51
|
+
*/
|
|
52
|
+
set(pressed) {
|
|
53
|
+
if (this.#disabled()) return
|
|
54
|
+
|
|
55
|
+
this.#commit(Boolean(pressed))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#commit(pressed) {
|
|
59
|
+
const change = this.dispatch("change", {
|
|
60
|
+
prefix: "poetry:toggle",
|
|
61
|
+
cancelable: true,
|
|
62
|
+
detail: { pressed }
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (change.defaultPrevented) return
|
|
66
|
+
|
|
67
|
+
this.element.setAttribute("aria-pressed", String(pressed))
|
|
68
|
+
setState(this.element, pressed ? "pressed" : "unpressed")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#disabled() {
|
|
72
|
+
return this.element.hasAttribute("disabled") || this.element.getAttribute("aria-disabled") === "true"
|
|
73
|
+
}
|
|
74
|
+
}
|