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,55 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { isEditingTarget, matchesHotkey } from "@poetry/controllers/helpers/hotkey"
|
|
3
|
+
|
|
4
|
+
// Declarative global shortcut:
|
|
5
|
+
// put the controller on any clickable element and declare the descriptor -
|
|
6
|
+
//
|
|
7
|
+
// <a href="/inbox" data-controller="poetry--core--hotkey"
|
|
8
|
+
// data-poetry--core--hotkey-keys-value="g+i">
|
|
9
|
+
//
|
|
10
|
+
// On match the controller dispatches a cancelable poetry--core--hotkey:pressed
|
|
11
|
+
// event, then clicks the host element (buttons, links, summary - anything
|
|
12
|
+
// click-activatable). Unmodified single-key descriptors stay inert while
|
|
13
|
+
// the user is typing in an input/textarea/select/contentEditable; combos
|
|
14
|
+
// carrying meta/ctrl/alt fire everywhere (the ⌘K convention).
|
|
15
|
+
export default class extends Controller {
|
|
16
|
+
static values = {
|
|
17
|
+
keys: { type: String, default: "" }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static events = ["poetry--core--hotkey:pressed"]
|
|
21
|
+
|
|
22
|
+
#onKeydown = null
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Arms the window keydown listener when a descriptor is declared (an
|
|
26
|
+
* empty keys value stays inert).
|
|
27
|
+
*/
|
|
28
|
+
connect() {
|
|
29
|
+
if (this.keysValue === "") return
|
|
30
|
+
|
|
31
|
+
// defaultPrevented gate: a key another hotkey consumer (dialog
|
|
32
|
+
// hotkey, another instance) already claimed stays claimed.
|
|
33
|
+
this.#onKeydown = (event) => {
|
|
34
|
+
if (event.defaultPrevented || !matchesHotkey(event, this.keysValue)) return
|
|
35
|
+
if (isEditingTarget(event) && !this.#modified(event)) return
|
|
36
|
+
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
const pressed = this.dispatch("pressed", { cancelable: true })
|
|
39
|
+
if (!pressed.defaultPrevented) this.element.click()
|
|
40
|
+
}
|
|
41
|
+
window.addEventListener("keydown", this.#onKeydown)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Disarms the listener. */
|
|
45
|
+
disconnect() {
|
|
46
|
+
if (!this.#onKeydown) return
|
|
47
|
+
|
|
48
|
+
window.removeEventListener("keydown", this.#onKeydown)
|
|
49
|
+
this.#onKeydown = null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#modified(event) {
|
|
53
|
+
return event.metaKey || event.ctrlKey || event.altKey
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { portalContent, resolvePortalContainer, restoreContent } from "@poetry/controllers/helpers/portal"
|
|
3
|
+
import { enterPresence, exitPresence } from "@poetry/controllers/helpers/presence"
|
|
4
|
+
import { setState, stateOf } from "@poetry/controllers/helpers/state"
|
|
5
|
+
import { tabbableWithin } from "@poetry/controllers/helpers/tabbable"
|
|
6
|
+
|
|
7
|
+
// The HoverCard controller (the popper-consumer trio's thinnest machine):
|
|
8
|
+
// pointer-only enrichment behind a LINK. Two timers (open 600 / close
|
|
9
|
+
// 300, over the trigger+content
|
|
10
|
+
// pair, re-enter cancels - the grace window, no polygon), the touch double-guard
|
|
11
|
+
// (pointerType 'touch' no-ops AND touchstart preventDefaults so a tap can
|
|
12
|
+
// never synthesize a focus-open - a tap just navigates the link), the focus
|
|
13
|
+
// mirror (trigger focus opens immediately / blur closes - a keyboard user
|
|
14
|
+
// SEES the card), the per-open TABINDEX STRIP (every tabbable inside is
|
|
15
|
+
// forced tabindex=-1: keyboard and touch users never reach inside,
|
|
16
|
+
// intentional and contractual - the reachable-elsewhere rule), and the
|
|
17
|
+
// SELECTION HOLD (text selection started in the card keeps it open and
|
|
18
|
+
// suppresses body user-select while dragging, so previews are copyable).
|
|
19
|
+
//
|
|
20
|
+
// NO focus-scope anywhere in the lifecycle: focus never moves in, so there
|
|
21
|
+
// is nothing to trap or restore - the trio's simplest teardown. The
|
|
22
|
+
// token-activated dismissable delivers Esc (topmost-only) + outside press
|
|
23
|
+
// while open. No aria surface is added (no haspopup/expanded/describedby):
|
|
24
|
+
// advertising a keyboard-unreachable surface to AT is worse than silence.
|
|
25
|
+
const TRIGGER_SELECTOR = '[data-slot="hover-card-trigger"]'
|
|
26
|
+
const CONTENT_SELECTOR = '[data-slot="hover-card-content"]'
|
|
27
|
+
|
|
28
|
+
const EVENT_PREFIX = "poetry:hover-card"
|
|
29
|
+
|
|
30
|
+
const DISMISSABLE = "poetry--core--dismissable"
|
|
31
|
+
const POPPER_STRATEGY = "data-poetry--core--popper-strategy-value"
|
|
32
|
+
|
|
33
|
+
export default class HoverCardController extends Controller {
|
|
34
|
+
// The events this controller dispatches (manifest surface;
|
|
35
|
+
// events_declaration.test.js enforces the list stays honest).
|
|
36
|
+
static events = ["poetry:hover-card:closed", "poetry:hover-card:open"]
|
|
37
|
+
|
|
38
|
+
static values = {
|
|
39
|
+
open: { type: Boolean, default: false },
|
|
40
|
+
openDelay: { type: Number, default: 600 },
|
|
41
|
+
closeDelay: { type: Number, default: 300 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#connected = false
|
|
45
|
+
#wired = []
|
|
46
|
+
#openTimer = null
|
|
47
|
+
#closeTimer = null
|
|
48
|
+
#cancelExit = null
|
|
49
|
+
#containSelection = false
|
|
50
|
+
#hasSelection = false
|
|
51
|
+
#previousBodyUserSelect = null
|
|
52
|
+
#onPointerup = () => this.#handlePointerup()
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Wires the content listeners (portal-safe) and reconciles a
|
|
56
|
+
* server-pinned card (layer + tabindex strip catch up; the DOM wins).
|
|
57
|
+
*/
|
|
58
|
+
connect() {
|
|
59
|
+
const content = this.#content()
|
|
60
|
+
|
|
61
|
+
if (content) this.#wireContent(content)
|
|
62
|
+
|
|
63
|
+
this.#connected = true
|
|
64
|
+
|
|
65
|
+
// Reconcile-on-connect: a server-pinned card (open: true) activates the
|
|
66
|
+
// layer + the tabindex strip; the DOM attributes win.
|
|
67
|
+
if (this.#isOpen()) {
|
|
68
|
+
this.#activate(content)
|
|
69
|
+
this.openValue = true
|
|
70
|
+
} else if (this.openValue) {
|
|
71
|
+
this.#show()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clears every timer, restores suppressed selection and portaled
|
|
77
|
+
* content (drop-never-strand), and unwires the listeners.
|
|
78
|
+
*/
|
|
79
|
+
disconnect() {
|
|
80
|
+
this.#connected = false
|
|
81
|
+
this.#clearOpenTimer()
|
|
82
|
+
this.#clearCloseTimer()
|
|
83
|
+
this.#cancelExit?.()
|
|
84
|
+
this.#cancelExit = null
|
|
85
|
+
this.#restoreBodyUserSelect() // never strand suppressed selection (teardown contract)
|
|
86
|
+
|
|
87
|
+
// Never leave content stranded at the container (drop-never-strand).
|
|
88
|
+
const content = this.#content()
|
|
89
|
+
|
|
90
|
+
if (content) restoreContent(content)
|
|
91
|
+
|
|
92
|
+
for (const [target, type, listener] of this.#wired) target.removeEventListener(type, listener)
|
|
93
|
+
|
|
94
|
+
this.#wired = []
|
|
95
|
+
document.removeEventListener("pointerup", this.#onPointerup)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stimulus value callback - controllable state: pinned previews (Turbo
|
|
100
|
+
* Stream / Outlet ownership).
|
|
101
|
+
*
|
|
102
|
+
* @param {boolean} value
|
|
103
|
+
*/
|
|
104
|
+
openValueChanged(value) {
|
|
105
|
+
if (!this.#connected) return
|
|
106
|
+
|
|
107
|
+
if (value && !this.#isOpen()) this.#show()
|
|
108
|
+
else if (!value && this.#isOpen()) this.#hide("none")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- trigger actions ---
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The trigger's pointerenter action: arms the open timer; touch
|
|
115
|
+
* pointerType is EXCLUDED - a tap navigates the
|
|
116
|
+
* link instead.
|
|
117
|
+
*
|
|
118
|
+
* @param {PointerEvent} event
|
|
119
|
+
*/
|
|
120
|
+
pointerEnter(event) {
|
|
121
|
+
if (event.pointerType === "touch") return
|
|
122
|
+
|
|
123
|
+
this.#hasSelection = false // the next pointer cycle clears the hold
|
|
124
|
+
this.#clearCloseTimer()
|
|
125
|
+
|
|
126
|
+
if (this.#isOpen() || this.#openTimer !== null) return
|
|
127
|
+
|
|
128
|
+
this.#openTimer = window.setTimeout(() => {
|
|
129
|
+
this.#openTimer = null
|
|
130
|
+
this.#show()
|
|
131
|
+
}, this.openDelayValue)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The trigger's pointerleave action: cancels a pending open; leaving
|
|
136
|
+
* toward the content keeps the card, anywhere else arms the close
|
|
137
|
+
* grace.
|
|
138
|
+
*
|
|
139
|
+
* @param {PointerEvent} event
|
|
140
|
+
*/
|
|
141
|
+
pointerLeave(event) {
|
|
142
|
+
if (event.pointerType === "touch") return
|
|
143
|
+
|
|
144
|
+
this.#clearOpenTimer()
|
|
145
|
+
|
|
146
|
+
const related = event.relatedTarget instanceof Element ? event.relatedTarget : null
|
|
147
|
+
|
|
148
|
+
if (related && this.#content()?.contains(related)) return
|
|
149
|
+
if (this.#isOpen()) this.#scheduleClose()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The trigger's focus action: opens IMMEDIATELY, skipping the timers -
|
|
154
|
+
* a keyboard user sees the preview even though they
|
|
155
|
+
* cannot enter it.
|
|
156
|
+
*/
|
|
157
|
+
focusOpen() {
|
|
158
|
+
this.#clearCloseTimer()
|
|
159
|
+
this.#show()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The trigger's blur action: closes immediately. */
|
|
163
|
+
blurClose() {
|
|
164
|
+
this.#clearOpenTimer()
|
|
165
|
+
|
|
166
|
+
// Blur closes immediately - the selection hold defers only the
|
|
167
|
+
// pointer-leave path. The close reason for the focus path: trigger-focus.
|
|
168
|
+
if (this.#isOpen()) this.#hide("trigger-focus")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The trigger's touchstart action - the touch guard: preventDefault so
|
|
173
|
+
* a tap can never synthesize a focus event (a focus-opened card on
|
|
174
|
+
* touch would be unreachable). The tap still navigates the
|
|
175
|
+
* link.
|
|
176
|
+
*
|
|
177
|
+
* @param {TouchEvent} event
|
|
178
|
+
*/
|
|
179
|
+
touchGuard(event) {
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- open / close ---
|
|
184
|
+
|
|
185
|
+
#show() {
|
|
186
|
+
const content = this.#content()
|
|
187
|
+
|
|
188
|
+
if (!content || this.#isOpen()) return
|
|
189
|
+
|
|
190
|
+
this.#cancelExit?.()
|
|
191
|
+
this.#cancelExit = null
|
|
192
|
+
this.#clearCloseTimer()
|
|
193
|
+
|
|
194
|
+
const trigger = this.#trigger()
|
|
195
|
+
|
|
196
|
+
// Portal-on-open: move BEFORE the
|
|
197
|
+
// enter presence (reparenting mid-animation restarts it), re-anchor
|
|
198
|
+
// absolute - static under compositor scroll, transform-immune.
|
|
199
|
+
portalContent(content, { container: resolvePortalContainer(this.element) })
|
|
200
|
+
this.element.setAttribute(POPPER_STRATEGY, "absolute")
|
|
201
|
+
|
|
202
|
+
content.hidden = false
|
|
203
|
+
if (trigger) setState(trigger, "popup-open")
|
|
204
|
+
enterPresence(content)
|
|
205
|
+
this.#activateLayer(content)
|
|
206
|
+
this.#stripTabbables(content)
|
|
207
|
+
this.openValue = true
|
|
208
|
+
|
|
209
|
+
queueMicrotask(() => {
|
|
210
|
+
if (!this.#isOpen()) return
|
|
211
|
+
|
|
212
|
+
this.dispatch("open", { prefix: EVENT_PREFIX, detail: {} })
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#hide(reason) {
|
|
217
|
+
const content = this.#content()
|
|
218
|
+
|
|
219
|
+
if (!content || !this.#isOpen()) return
|
|
220
|
+
|
|
221
|
+
this.#clearOpenTimer()
|
|
222
|
+
this.#clearCloseTimer()
|
|
223
|
+
this.#hasSelection = false
|
|
224
|
+
this.#restoreBodyUserSelect()
|
|
225
|
+
|
|
226
|
+
const trigger = this.#trigger()
|
|
227
|
+
|
|
228
|
+
if (trigger) setState(trigger, "popup-closed")
|
|
229
|
+
this.openValue = false
|
|
230
|
+
|
|
231
|
+
this.#cancelExit = exitPresence(content, {
|
|
232
|
+
onRemove: () => {
|
|
233
|
+
this.#cancelExit = null
|
|
234
|
+
content.hidden = true
|
|
235
|
+
// Home AFTER the exit finished and hidden landed.
|
|
236
|
+
restoreContent(content)
|
|
237
|
+
this.element.setAttribute(POPPER_STRATEGY, "fixed")
|
|
238
|
+
this.#removeControllers(content, [DISMISSABLE])
|
|
239
|
+
this.dispatch("closed", { prefix: EVENT_PREFIX, detail: { reason } })
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Reconcile-on-connect for a server-pinned card: layer + strip catch up.
|
|
245
|
+
#activate(content) {
|
|
246
|
+
if (!content) return
|
|
247
|
+
|
|
248
|
+
this.#activateLayer(content)
|
|
249
|
+
this.#stripTabbables(content)
|
|
250
|
+
|
|
251
|
+
// ONE FRAME LATE (the tooltip's reconcile rule): portaling before the
|
|
252
|
+
// sibling popper's connect would rob it of its content target before
|
|
253
|
+
// it could cache the node.
|
|
254
|
+
window.requestAnimationFrame(() => {
|
|
255
|
+
if (!this.#connected || !this.#isOpen()) return
|
|
256
|
+
|
|
257
|
+
portalContent(content, { container: resolvePortalContainer(this.element) })
|
|
258
|
+
this.element.setAttribute(POPPER_STRATEGY, "absolute")
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#activateLayer(content) {
|
|
263
|
+
content.setAttribute(`data-${DISMISSABLE}-disable-outside-pointer-events-value`, "false")
|
|
264
|
+
this.#addControllers(content, [DISMISSABLE])
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The tabindex strip (contractual; per-open so stream-appended content
|
|
268
|
+
// re-strips on the next open): the card is sighted-pointer-only BY
|
|
269
|
+
// DESIGN - Tab passes straight over its contents. The shared tabbable
|
|
270
|
+
// walk already skips disabled/hidden/-1 elements.
|
|
271
|
+
#stripTabbables(content) {
|
|
272
|
+
for (const element of tabbableWithin(content)) element.setAttribute("tabindex", "-1")
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- the close grace (the trigger+content pair) ---
|
|
276
|
+
|
|
277
|
+
#scheduleClose() {
|
|
278
|
+
this.#clearCloseTimer()
|
|
279
|
+
this.#closeTimer = window.setTimeout(() => {
|
|
280
|
+
this.#closeTimer = null
|
|
281
|
+
|
|
282
|
+
// The selection hold: while a selection made from the card exists,
|
|
283
|
+
// pointer-leave does NOT close (the user is reading/copying).
|
|
284
|
+
if (this.#hasSelection) return
|
|
285
|
+
|
|
286
|
+
this.#hide("trigger-hover")
|
|
287
|
+
}, this.closeDelayValue)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#clearOpenTimer() {
|
|
291
|
+
if (this.#openTimer === null) return
|
|
292
|
+
|
|
293
|
+
window.clearTimeout(this.#openTimer)
|
|
294
|
+
this.#openTimer = null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#clearCloseTimer() {
|
|
298
|
+
if (this.#closeTimer === null) return
|
|
299
|
+
|
|
300
|
+
window.clearTimeout(this.#closeTimer)
|
|
301
|
+
this.#closeTimer = null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// --- content wiring (programmatic: portal-safe) ---
|
|
305
|
+
|
|
306
|
+
#wireContent(content) {
|
|
307
|
+
this.#listen(content, "pointerenter", (event) => {
|
|
308
|
+
if (event.pointerType === "touch") return
|
|
309
|
+
|
|
310
|
+
this.#clearCloseTimer()
|
|
311
|
+
})
|
|
312
|
+
this.#listen(content, "pointerleave", (event) => {
|
|
313
|
+
if (event.pointerType === "touch") return
|
|
314
|
+
|
|
315
|
+
const related = event.relatedTarget instanceof Element ? event.relatedTarget : null
|
|
316
|
+
|
|
317
|
+
if (related && (this.#trigger()?.contains(related) || content.contains(related))) return
|
|
318
|
+
if (this.#isOpen()) this.#scheduleClose()
|
|
319
|
+
})
|
|
320
|
+
this.#listen(content, "pointerdown", (event) => this.#handleContentPointerdown(event))
|
|
321
|
+
this.#listen(content, `${DISMISSABLE}:dismiss`, this.#onDismiss)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#listen(target, type, listener) {
|
|
325
|
+
target.addEventListener(type, listener)
|
|
326
|
+
this.#wired.push([target, type, listener])
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Esc (topmost-only) + pointerdown-outside arrive as the dismissable's
|
|
330
|
+
// dismiss event. Esc/outside ALWAYS close - the selection hold defers
|
|
331
|
+
// only the pointer-leave path. Focus never moved in, so there is nothing
|
|
332
|
+
// to restore.
|
|
333
|
+
#onDismiss = (event) => {
|
|
334
|
+
if (event.target !== this.#content()) return
|
|
335
|
+
|
|
336
|
+
const escaped = event.detail?.originalEvent?.type === "keydown"
|
|
337
|
+
|
|
338
|
+
this.#hide(escaped ? "escape-key" : "outside-press")
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --- the selection hold (adapted - source and license in
|
|
342
|
+
// THIRD_PARTY_NOTICES.md) ---
|
|
343
|
+
|
|
344
|
+
// pointerdown ON the content: suppress body user-select (webkit prefix
|
|
345
|
+
// included) so the drag selects only card text; pointerup restores it
|
|
346
|
+
// and, one frame later, records whether a selection exists.
|
|
347
|
+
#handleContentPointerdown(event) {
|
|
348
|
+
if (event.pointerType === "touch") return
|
|
349
|
+
|
|
350
|
+
this.#containSelection = true
|
|
351
|
+
this.#suppressBodyUserSelect()
|
|
352
|
+
document.addEventListener("pointerup", this.#onPointerup, { once: true })
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#handlePointerup() {
|
|
356
|
+
if (!this.#containSelection) return
|
|
357
|
+
|
|
358
|
+
this.#containSelection = false
|
|
359
|
+
this.#restoreBodyUserSelect()
|
|
360
|
+
|
|
361
|
+
window.setTimeout(() => {
|
|
362
|
+
this.#hasSelection = (document.getSelection()?.toString() ?? "") !== ""
|
|
363
|
+
}, 0)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#suppressBodyUserSelect() {
|
|
367
|
+
if (this.#previousBodyUserSelect !== null) return
|
|
368
|
+
|
|
369
|
+
const { body } = document
|
|
370
|
+
|
|
371
|
+
this.#previousBodyUserSelect = {
|
|
372
|
+
userSelect: body.style.userSelect ?? "",
|
|
373
|
+
webkitUserSelect: body.style.webkitUserSelect ?? ""
|
|
374
|
+
}
|
|
375
|
+
body.style.userSelect = "none"
|
|
376
|
+
body.style.webkitUserSelect = "none"
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#restoreBodyUserSelect() {
|
|
380
|
+
if (this.#previousBodyUserSelect === null) return
|
|
381
|
+
|
|
382
|
+
const { body } = document
|
|
383
|
+
|
|
384
|
+
body.style.userSelect = this.#previousBodyUserSelect.userSelect
|
|
385
|
+
body.style.webkitUserSelect = this.#previousBodyUserSelect.webkitUserSelect
|
|
386
|
+
this.#previousBodyUserSelect = null
|
|
387
|
+
this.#containSelection = false
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// --- structural resolution (the server id pair, portal-safe) ---
|
|
391
|
+
|
|
392
|
+
#trigger() {
|
|
393
|
+
return this.element.querySelector(TRIGGER_SELECTOR)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#content() {
|
|
397
|
+
const triggerId = this.#trigger()?.id
|
|
398
|
+
|
|
399
|
+
if (triggerId?.endsWith("-trigger")) {
|
|
400
|
+
const byId = document.getElementById(triggerId.replace(/-trigger$/, "-content"))
|
|
401
|
+
|
|
402
|
+
if (byId) return byId
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return this.element.querySelector(CONTENT_SELECTOR)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#isOpen() {
|
|
409
|
+
const content = this.#content()
|
|
410
|
+
|
|
411
|
+
return Boolean(content) && stateOf(content) === "open"
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#addControllers(element, identifiers) {
|
|
415
|
+
const tokens = (element.getAttribute("data-controller") ?? "").split(/\s+/).filter(Boolean)
|
|
416
|
+
|
|
417
|
+
for (const identifier of identifiers) {
|
|
418
|
+
if (!tokens.includes(identifier)) tokens.push(identifier)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
element.setAttribute("data-controller", tokens.join(" "))
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#removeControllers(element, identifiers) {
|
|
425
|
+
const tokens = (element.getAttribute("data-controller") ?? "")
|
|
426
|
+
.split(/\s+/)
|
|
427
|
+
.filter((token) => token && !identifiers.includes(token))
|
|
428
|
+
|
|
429
|
+
element.setAttribute("data-controller", tokens.join(" "))
|
|
430
|
+
}
|
|
431
|
+
}
|