poetry-core 0.1.0 → 0.1.1
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 +20 -1
- data/app/components/poetry/core/component.rb +97 -2
- data/app/javascript/poetry/core/drawer_controller.js +28 -0
- data/app/javascript/poetry/core/hover_card_controller.js +26 -11
- data/app/javascript/poetry/core/message_scroller_controller.js +82 -4
- data/app/javascript/poetry/core/navigation_menu_controller.js +59 -4
- data/app/javascript/poetry/core/number_field_controller.js +24 -14
- data/config/controllers_manifest.json +2 -3
- data/lib/poetry/core/check.rb +27 -10
- data/lib/poetry/core/config.rb +5 -2
- data/lib/poetry/core/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b4189654db2722eff128793c1bd4ce725c06a60ec1b7ff8bf29d5af7aa4e9ef
|
|
4
|
+
data.tar.gz: fb0245f3b9abff92ad3f9bdd48d3da7a5fc421531e346d40e3e53eadaf304233
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c79e20564e016e6b3b7b34378bb6222ca8708279864235bfd0c8d2a7b899f56e229a53cb284dbec9b9601e91922fc6f9799353fc8c7ddac8d1fa5d8fba6ae0b
|
|
7
|
+
data.tar.gz: 73b7dc73529fb2c1782a343ef7ddac9228591fba3b73c57163a0cfab4f6066e26da0d573447af273605e0bc98e1ff40d0fe0917a770c56e873ec22714b563182
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.1.1] - 2026-09-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `identity:` on every component: the one sanctioned way for a component that renders as another component's root to name it (its `data-component`). Universal like `key:`, never an HTML attribute.
|
|
8
|
+
- `poetry check` rule `reserved-attribute`: `data-component` passed through a helper, as a string key or `data: { component: }`, is an error.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- The passthrough contract is enforced at render. A keyword that is not an option still renders as an HTML attribute on the root, but a near miss of a declared option (`varient:`) raises in development and test with a did-you-mean and logs in production; `data-component` is never overridable (raised in development and test, dropped in production); `data-slot` stays open as a composition seam. `poetry check`'s `unknown-option` finding is an error, no longer a warning.
|
|
13
|
+
- `poetry--core--hover-card`: `touchGuard` (on touchstart) is replaced by `pointerDown` (on pointerdown). `poetry--core--number-field`: the `focus` action is removed. A host that wired either by hand updates the action strings; poetry-ui's components already have.
|
|
14
|
+
- `css_mode :bem` is documented as the mode for kits authored on the DSL that write their own templates; poetry-ui is Tailwind-native and not a `:bem` consumer.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- HoverCard: a tap on the trigger keeps its click on touch devices. The touchstart guard cancelled the click; a pointerdown latch held through the tap's compatibility mouse events replaces it.
|
|
19
|
+
- MessageScroller: `data-pending-scroll` holds the root and the viewport until the opening position (`end` or `last-anchor`) is applied, releases at once for an empty transcript, is stripped when a Turbo morph re-stamps it, and is re-armed at `turbo:before-cache`, so a server-rendered or restored transcript never shows the top of the thread first.
|
|
20
|
+
- NumberField: a sideways trackpad gesture over a wheel-enabled field scrolls the page instead of stepping, Shift on the horizontal axis steps large, and an event with no movement is neither stepped nor cancelled. Focus keeps the browser's own selection (Tab selects the value, a click places the caret); the steppers park the caret at the end.
|
|
21
|
+
- NavigationMenu: a disabled trigger never opens, by hover or click, and the arrows step over it; ArrowDown on a trigger opens its panel with focus staying on the trigger; removing the open trigger's item, as a morph can, closes the bar and drops its orphaned panel.
|
|
22
|
+
- Drawer: the click that follows a press inside the panel never dismisses, so a drag against the clamp released over the backdrop no longer closes the sheet.
|
|
4
23
|
|
|
5
24
|
## [0.1.0] - 2026-09-05
|
|
6
25
|
|
|
@@ -145,14 +145,36 @@ module Poetry
|
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
# Keywords that are never options and never typos - the passthrough
|
|
149
|
+
# vocabulary the static check exempts too (Check::Catalog::PASSTHROUGH).
|
|
150
|
+
PASSTHROUGH_KEYS = %w[class id key webmcp identity data aria role style].freeze
|
|
151
|
+
|
|
152
|
+
# HTML attribute names a caller passes through on purpose. Exempt from
|
|
153
|
+
# the near-miss guard so `for:` never reads as a typo of a `form`
|
|
154
|
+
# option, `size:` on a component without a size axis stays the native
|
|
155
|
+
# attribute, and so on. Only keys that are NOT declared options reach
|
|
156
|
+
# the guard, so a declared option of the same name is untouched.
|
|
157
|
+
HTML_ATTRIBUTE_KEYS = %w[
|
|
158
|
+
accept accesskey action alt autocapitalize autocomplete autofocus checked cite cols colspan
|
|
159
|
+
contenteditable crossorigin datetime decoding dir dirname disabled download draggable enctype
|
|
160
|
+
enterkeyhint for form headers height hidden href hreflang inert inputmode is itemid itemprop
|
|
161
|
+
itemref itemscope itemtype label lang list loading max maxlength method min minlength multiple
|
|
162
|
+
name nonce novalidate open part pattern ping placeholder popover readonly referrerpolicy rel
|
|
163
|
+
required reversed rows rowspan scope selected size sizes slot span spellcheck src srcset start
|
|
164
|
+
step tabindex target title translate type value width wrap
|
|
165
|
+
].freeze
|
|
166
|
+
|
|
148
167
|
# The self-identification markup contract, the convention every
|
|
149
168
|
# component follows: `data-component` on the component root maps live DOM
|
|
150
169
|
# back to the component that rendered it - the hook agents, the
|
|
151
170
|
# Verifier, and the browser-verification loop key on.
|
|
152
171
|
#
|
|
172
|
+
# A component rendering as another's root passes identity: (the
|
|
173
|
+
# composition seam), and the root wears that name instead.
|
|
174
|
+
#
|
|
153
175
|
# @return [Hash] e.g. { "data-component" => "button" }
|
|
154
176
|
def component_data_attributes
|
|
155
|
-
{ "data-component" => self.class.component_title }
|
|
177
|
+
{ "data-component" => (@identity || self.class.component_title).to_s }
|
|
156
178
|
end
|
|
157
179
|
|
|
158
180
|
# `data-slot` for a named part of the component's anatomy
|
|
@@ -224,6 +246,14 @@ module Poetry
|
|
|
224
246
|
@webmcp = attributes[:webmcp] || attributes["webmcp"]
|
|
225
247
|
attributes = attributes.except(:webmcp, "webmcp") unless @webmcp.nil?
|
|
226
248
|
|
|
249
|
+
# identity: is the composition seam for a component that renders AS
|
|
250
|
+
# another component's root (ToastTrigger renders as a Button): the
|
|
251
|
+
# inner root wears the outer's data-component. Universal like key:,
|
|
252
|
+
# never an HTML attribute - and the ONLY sanctioned way to set
|
|
253
|
+
# data-component (the raw attribute is reserved, see guard_passthrough).
|
|
254
|
+
@identity = attributes[:identity] || attributes["identity"]
|
|
255
|
+
attributes = attributes.except(:identity, "identity") unless @identity.nil?
|
|
256
|
+
|
|
227
257
|
# Initialize a fresh Set for this instance
|
|
228
258
|
self.registered_styles = Set.new
|
|
229
259
|
self.registered_options = Set.new
|
|
@@ -249,12 +279,77 @@ module Poetry
|
|
|
249
279
|
end
|
|
250
280
|
|
|
251
281
|
@attributes = self.class._default_attributes.deep_dup
|
|
252
|
-
html_attrs = attributes.with_indifferent_access.except(*attribute_names)
|
|
282
|
+
html_attrs = guard_passthrough(attributes.with_indifferent_access.except(*attribute_names))
|
|
253
283
|
@html_attributes = Poetry::Core::HTML::Attributes.new(html_attrs)
|
|
254
284
|
|
|
255
285
|
assign_attributes attributes.with_indifferent_access.slice(*attribute_names)
|
|
256
286
|
end
|
|
257
287
|
|
|
288
|
+
# The passthrough contract, enforced at the seam. A keyword that is not
|
|
289
|
+
# an option renders as an HTML attribute on the root (title:, tabindex:,
|
|
290
|
+
# colspan:) - so a NEAR-MISS of a declared option (varient:) would
|
|
291
|
+
# silently render a bogus attribute while the default applied. That
|
|
292
|
+
# raises in development and test with the did-you-mean the static check
|
|
293
|
+
# gives, and only logs in production (the render still succeeds).
|
|
294
|
+
# data-component is the component's own identity - the hook agents,
|
|
295
|
+
# the Verifier and the browser loop key on - and is never overridable:
|
|
296
|
+
# dropped in production, raised the same way in development and test.
|
|
297
|
+
# data-slot is NOT reserved: re-slotting an embedded root is the
|
|
298
|
+
# composition seam a part contract allows; re-identifying one goes
|
|
299
|
+
# through identity:, the sanctioned spelling.
|
|
300
|
+
#
|
|
301
|
+
# @param html_attrs [ActiveSupport::HashWithIndifferentAccess]
|
|
302
|
+
# @return [ActiveSupport::HashWithIndifferentAccess] the attributes, minus a reserved override
|
|
303
|
+
def guard_passthrough(html_attrs)
|
|
304
|
+
problems = []
|
|
305
|
+
|
|
306
|
+
if html_attrs.key?("data-component")
|
|
307
|
+
problems << "data-component is #{passthrough_owner}'s own identity attribute and is never overridable"
|
|
308
|
+
html_attrs = html_attrs.except("data-component")
|
|
309
|
+
end
|
|
310
|
+
data = html_attrs["data"]
|
|
311
|
+
if data.respond_to?(:key?) && data.key?("component")
|
|
312
|
+
problems << "data: { component: } is #{passthrough_owner}'s own identity attribute and is never overridable"
|
|
313
|
+
html_attrs["data"] = data.except("component")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
html_attrs.each_key do |key|
|
|
317
|
+
next if PASSTHROUGH_KEYS.include?(key) || HTML_ATTRIBUTE_KEYS.include?(key) || key.include?("-")
|
|
318
|
+
|
|
319
|
+
suggestion = option_suggestion(key)
|
|
320
|
+
next unless suggestion
|
|
321
|
+
|
|
322
|
+
problems << "#{passthrough_owner} has no option #{key}: (did you mean #{suggestion}:?) - " \
|
|
323
|
+
"unknown keywords render as HTML attributes"
|
|
324
|
+
end
|
|
325
|
+
return html_attrs if problems.empty?
|
|
326
|
+
|
|
327
|
+
raise ArgumentError, problems.join("; ") if strict_passthrough?
|
|
328
|
+
|
|
329
|
+
Rails.logger&.warn("poetry: #{problems.join("; ")}") if defined?(Rails) && Rails.respond_to?(:logger)
|
|
330
|
+
html_attrs
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# The did-you-mean against this component's declared options and
|
|
334
|
+
# styles, the same checker the static check runs.
|
|
335
|
+
def option_suggestion(key)
|
|
336
|
+
require "did_you_mean"
|
|
337
|
+
DidYouMean::SpellChecker.new(dictionary: self.class.attribute_names.map(&:to_s)).correct(key.to_s).first
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# The component named in a passthrough problem - an anonymous class
|
|
341
|
+
# (a test double) has no path to title.
|
|
342
|
+
def passthrough_owner
|
|
343
|
+
self.class.name ? self.class.component_title : "this component"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Raise (development, test) or log (everything else).
|
|
347
|
+
def strict_passthrough?
|
|
348
|
+
defined?(Rails) && Rails.respond_to?(:env) && Rails.env.local?
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
private :guard_passthrough, :option_suggestion, :passthrough_owner, :strict_passthrough?
|
|
352
|
+
|
|
258
353
|
# Returns all component attributes, ensuring proc defaults are evaluated.
|
|
259
354
|
#
|
|
260
355
|
# This method overrides ActiveModel's attributes method to trigger evaluation
|
|
@@ -49,6 +49,10 @@ export default class DrawerController extends DialogController {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
#swipe = null
|
|
52
|
+
// Where the last press landed (inside the panel or on the backdrop) -
|
|
53
|
+
// the click that follows a press is never a backdrop dismissal when the
|
|
54
|
+
// press started inside, however far the pointer travelled since.
|
|
55
|
+
#pressInside = false
|
|
52
56
|
#closing = false
|
|
53
57
|
#snapIndex = 0
|
|
54
58
|
|
|
@@ -114,6 +118,29 @@ export default class DrawerController extends DialogController {
|
|
|
114
118
|
})
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The inherited backdrop click, minus the ghost: a press that started
|
|
123
|
+
* inside the panel can end over the backdrop (a drag against the
|
|
124
|
+
* clamp, a slip off an inner control) and the click the browser then
|
|
125
|
+
* fires is a backdrop click by target and coordinates, never by intent.
|
|
126
|
+
*
|
|
127
|
+
* @param {MouseEvent} event
|
|
128
|
+
*/
|
|
129
|
+
backdropClose(event) {
|
|
130
|
+
if (this.#pressInside) return
|
|
131
|
+
|
|
132
|
+
super.backdropClose(event)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The dialog's own inside test (a press on the backdrop targets the
|
|
136
|
+
// <dialog> too - only the rect tells them apart).
|
|
137
|
+
#insidePanel(event) {
|
|
138
|
+
const rect = this.dialogTarget.getBoundingClientRect()
|
|
139
|
+
|
|
140
|
+
return rect.top <= event.clientY && event.clientY <= rect.bottom &&
|
|
141
|
+
rect.left <= event.clientX && event.clientX <= rect.right
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
// --- the swipe -----------------------------------------------------------
|
|
118
145
|
|
|
119
146
|
/**
|
|
@@ -124,6 +151,7 @@ export default class DrawerController extends DialogController {
|
|
|
124
151
|
* @param {PointerEvent} event
|
|
125
152
|
*/
|
|
126
153
|
swipeStart(event) {
|
|
154
|
+
this.#pressInside = this.#insidePanel(event)
|
|
127
155
|
if (event.button !== 0 && event.pointerType === "mouse") return
|
|
128
156
|
if (this.#closing) return
|
|
129
157
|
|
|
@@ -8,8 +8,9 @@ import { tabbableWithin } from "@poetry/controllers/helpers/tabbable"
|
|
|
8
8
|
// pointer-only enrichment behind a LINK. Two timers (open 600 / close
|
|
9
9
|
// 300, over the trigger+content
|
|
10
10
|
// pair, re-enter cancels - the grace window, no polygon), the touch double-guard
|
|
11
|
-
// (pointerType 'touch' no-ops AND
|
|
12
|
-
//
|
|
11
|
+
// (pointerType 'touch' no-ops AND the pointerdown latch swallows the focus a
|
|
12
|
+
// tap or click causes, so a tap keeps its click and just navigates the
|
|
13
|
+
// link - cancelling touchstart would cancel that click with it), the focus
|
|
13
14
|
// mirror (trigger focus opens immediately / blur closes - a keyboard user
|
|
14
15
|
// SEES the card), the per-open TABINDEX STRIP (every tabbable inside is
|
|
15
16
|
// forced tabindex=-1: keyboard and touch users never reach inside,
|
|
@@ -28,6 +29,11 @@ const CONTENT_SELECTOR = '[data-slot="hover-card-content"]'
|
|
|
28
29
|
const EVENT_PREFIX = "poetry:hover-card"
|
|
29
30
|
|
|
30
31
|
const DISMISSABLE = "poetry--core--dismissable"
|
|
32
|
+
// The pointerdown latch releases once the press has fully landed: the
|
|
33
|
+
// compatibility mouseup (a tap dispatches its mouse events, focus among
|
|
34
|
+
// them, AFTER its pointerup - so pointerup is too early), a cancelled
|
|
35
|
+
// touch, or any key (keyboard use is never mid-press).
|
|
36
|
+
const LATCH_RELEASE_EVENTS = ["mouseup", "pointercancel", "keydown"]
|
|
31
37
|
const POPPER_STRATEGY = "data-poetry--core--popper-strategy-value"
|
|
32
38
|
|
|
33
39
|
export default class HoverCardController extends Controller {
|
|
@@ -49,7 +55,12 @@ export default class HoverCardController extends Controller {
|
|
|
49
55
|
#containSelection = false
|
|
50
56
|
#hasSelection = false
|
|
51
57
|
#previousBodyUserSelect = null
|
|
58
|
+
#isPointerDown = false
|
|
52
59
|
#onPointerup = () => this.#handlePointerup()
|
|
60
|
+
#releaseLatch = () => {
|
|
61
|
+
this.#isPointerDown = false
|
|
62
|
+
for (const type of LATCH_RELEASE_EVENTS) document.removeEventListener(type, this.#releaseLatch)
|
|
63
|
+
}
|
|
53
64
|
|
|
54
65
|
/**
|
|
55
66
|
* Wires the content listeners (portal-safe) and reconciles a
|
|
@@ -93,6 +104,7 @@ export default class HoverCardController extends Controller {
|
|
|
93
104
|
|
|
94
105
|
this.#wired = []
|
|
95
106
|
document.removeEventListener("pointerup", this.#onPointerup)
|
|
107
|
+
this.#releaseLatch()
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
/**
|
|
@@ -152,9 +164,11 @@ export default class HoverCardController extends Controller {
|
|
|
152
164
|
/**
|
|
153
165
|
* The trigger's focus action: opens IMMEDIATELY, skipping the timers -
|
|
154
166
|
* a keyboard user sees the preview even though they
|
|
155
|
-
* cannot enter it.
|
|
167
|
+
* cannot enter it. Focus a pointer caused (the latch) does not open.
|
|
156
168
|
*/
|
|
157
169
|
focusOpen() {
|
|
170
|
+
if (this.#isPointerDown) return
|
|
171
|
+
|
|
158
172
|
this.#clearCloseTimer()
|
|
159
173
|
this.#show()
|
|
160
174
|
}
|
|
@@ -169,15 +183,16 @@ export default class HoverCardController extends Controller {
|
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
/**
|
|
172
|
-
* The trigger's
|
|
173
|
-
*
|
|
174
|
-
* touch would be unreachable)
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
186
|
+
* The trigger's pointerdown action - the latch: the focus a tap or a
|
|
187
|
+
* click causes must not open the card (a pointer user never gets a
|
|
188
|
+
* focus-opened card; on touch it would be unreachable), so focusOpen
|
|
189
|
+
* stays shut until the press has landed (LATCH_RELEASE_EVENTS). The
|
|
190
|
+
* event is never cancelled: cancelling touchstart would cancel the
|
|
191
|
+
* tap's click, and the tap must still navigate the link.
|
|
178
192
|
*/
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
pointerDown() {
|
|
194
|
+
this.#isPointerDown = true
|
|
195
|
+
for (const type of LATCH_RELEASE_EVENTS) document.addEventListener(type, this.#releaseLatch)
|
|
181
196
|
}
|
|
182
197
|
|
|
183
198
|
// --- open / close ---
|
|
@@ -27,6 +27,17 @@ import {
|
|
|
27
27
|
// auto-scroll animation cannot release itself.
|
|
28
28
|
const AUTOSCROLLING_CLEAR_DELAY = 180
|
|
29
29
|
|
|
30
|
+
// The opening-position hold: data-pending-scroll sits on the root and the
|
|
31
|
+
// viewport until defaultScrollPosition end / last-anchor is applied (or
|
|
32
|
+
// skipped, on an empty transcript), so the dictionary can hide the viewport
|
|
33
|
+
// instead of painting the top of the thread and then jumping. The server
|
|
34
|
+
// renders the attribute for those two positions (a scroll container always
|
|
35
|
+
// opens at the top, and the first paint must not wait for this controller);
|
|
36
|
+
// from connect on the controller owns it. Mount-only: a live value change
|
|
37
|
+
// re-applies the position without hiding the viewport again.
|
|
38
|
+
const PENDING_SCROLL_ATTRIBUTE = "data-pending-scroll"
|
|
39
|
+
const HELD_SCROLL_POSITIONS = new Set(["end", "last-anchor"])
|
|
40
|
+
|
|
30
41
|
// Viewport keys that count as deliberate scroll intent and release follow.
|
|
31
42
|
const USER_SCROLL_KEYS = new Set([
|
|
32
43
|
"ArrowDown",
|
|
@@ -56,6 +67,10 @@ const USER_SCROLL_KEYS = new Set([
|
|
|
56
67
|
// anchored-to-message a turn held at the reading line while a reply streams
|
|
57
68
|
// settling-jump a programmatic jump animating; intent suppressed
|
|
58
69
|
//
|
|
70
|
+
// Presence attributes mirrored on the root AND the viewport: data-scrollable
|
|
71
|
+
// (which edges have room), data-autoscrolling (a programmatic scroll is
|
|
72
|
+
// settling), data-pending-scroll (the opening-position hold, above).
|
|
73
|
+
//
|
|
59
74
|
// Viewport scroll/wheel/touchmove/keydown listeners are wired here (passive
|
|
60
75
|
// flags need addEventListener) - do NOT also declare them as data-actions.
|
|
61
76
|
// The jump button IS a data-action: click->...#jump.
|
|
@@ -101,6 +116,7 @@ export default class extends Controller {
|
|
|
101
116
|
this.prependRestore = null
|
|
102
117
|
this.pendingScrollToMessage = null
|
|
103
118
|
this.defaultScrollPositionApplied = false
|
|
119
|
+
this.pendingScroll = HELD_SCROLL_POSITIONS.has(this.defaultScrollPositionValue)
|
|
104
120
|
this.spacerHeight = 0
|
|
105
121
|
this.spacerGap = getFlexGap(this.#spacer()?.parentElement ?? null)
|
|
106
122
|
this.handledScrollAnchors = new WeakSet()
|
|
@@ -150,6 +166,16 @@ export default class extends Controller {
|
|
|
150
166
|
// defaultScrollPosition once, commits scrollable state.
|
|
151
167
|
this.#handleContentChange()
|
|
152
168
|
|
|
169
|
+
// The hold releases with the opening position (inside the mount pass),
|
|
170
|
+
// or right away when there is nothing to scroll. A Turbo morph that
|
|
171
|
+
// re-stamps the server's attribute afterwards is stripped by the
|
|
172
|
+
// observer; a cache snapshot re-arms it (see #rearmPendingScroll).
|
|
173
|
+
if (this.itemCount === 0) this.pendingScroll = false
|
|
174
|
+
this.#writePendingScroll()
|
|
175
|
+
this.#observePendingScroll(viewport)
|
|
176
|
+
this.onBeforeCache = () => this.#rearmPendingScroll()
|
|
177
|
+
document.addEventListener("turbo:before-cache", this.onBeforeCache)
|
|
178
|
+
|
|
153
179
|
if (this.trackVisibilityValue) this.#observeVisibility()
|
|
154
180
|
|
|
155
181
|
this.started = true
|
|
@@ -189,6 +215,9 @@ export default class extends Controller {
|
|
|
189
215
|
this.contentResizeObserver = null
|
|
190
216
|
this.visibilityObserver?.disconnect()
|
|
191
217
|
this.visibilityObserver = null
|
|
218
|
+
this.pendingScrollObserver?.disconnect()
|
|
219
|
+
this.pendingScrollObserver = null
|
|
220
|
+
document.removeEventListener("turbo:before-cache", this.onBeforeCache)
|
|
192
221
|
this.observedRows.clear()
|
|
193
222
|
this.visibleMessageIds.clear()
|
|
194
223
|
|
|
@@ -661,7 +690,7 @@ export default class extends Controller {
|
|
|
661
690
|
|
|
662
691
|
if (!handled) return false
|
|
663
692
|
|
|
664
|
-
this
|
|
693
|
+
this.#markDefaultScrollPositionApplied()
|
|
665
694
|
|
|
666
695
|
return true
|
|
667
696
|
}
|
|
@@ -708,6 +737,55 @@ export default class extends Controller {
|
|
|
708
737
|
return true
|
|
709
738
|
}
|
|
710
739
|
|
|
740
|
+
// --- the opening-position hold ---
|
|
741
|
+
|
|
742
|
+
#markDefaultScrollPositionApplied() {
|
|
743
|
+
this.defaultScrollPositionApplied = true
|
|
744
|
+
|
|
745
|
+
if (!this.pendingScroll) return
|
|
746
|
+
|
|
747
|
+
this.pendingScroll = false
|
|
748
|
+
this.#writePendingScroll()
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#writePendingScroll() {
|
|
752
|
+
for (const element of [this.element, this.#viewport()]) {
|
|
753
|
+
if (!element) continue
|
|
754
|
+
|
|
755
|
+
if (this.pendingScroll) element.setAttribute(PENDING_SCROLL_ATTRIBUTE, "")
|
|
756
|
+
else element.removeAttribute(PENDING_SCROLL_ATTRIBUTE)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// A Turbo morph re-stamps the server-rendered attribute onto the live
|
|
761
|
+
// elements after the position already applied: strip it. Never re-add
|
|
762
|
+
// through this path - an inline script that scrolled and released the
|
|
763
|
+
// hold before connect must keep its release.
|
|
764
|
+
#observePendingScroll(viewport) {
|
|
765
|
+
if (typeof MutationObserver === "undefined") return
|
|
766
|
+
|
|
767
|
+
this.pendingScrollObserver = new MutationObserver(() => {
|
|
768
|
+
if (!this.pendingScroll) this.#writePendingScroll()
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
for (const element of [this.element, viewport]) {
|
|
772
|
+
this.pendingScrollObserver.observe(element, {
|
|
773
|
+
attributes: true,
|
|
774
|
+
attributeFilter: [PENDING_SCROLL_ATTRIBUTE]
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// A cached snapshot clones the elements without their scroll position, so
|
|
780
|
+
// a restore would paint the top of the thread until reconnect re-applies
|
|
781
|
+
// the opening position: re-arm the hold before Turbo caches the page.
|
|
782
|
+
#rearmPendingScroll() {
|
|
783
|
+
if (!HELD_SCROLL_POSITIONS.has(this.defaultScrollPositionValue)) return
|
|
784
|
+
|
|
785
|
+
this.pendingScroll = true
|
|
786
|
+
this.#writePendingScroll()
|
|
787
|
+
}
|
|
788
|
+
|
|
711
789
|
// --- scroll commands (mechanics, split from the policy above as in source) ---
|
|
712
790
|
|
|
713
791
|
#setAutoScrolling(autoscrolling) {
|
|
@@ -878,7 +956,7 @@ export default class extends Controller {
|
|
|
878
956
|
if (!element) {
|
|
879
957
|
if (this.itemCount === 0) {
|
|
880
958
|
this.pendingScrollToMessage = { messageId, options }
|
|
881
|
-
this
|
|
959
|
+
this.#markDefaultScrollPositionApplied()
|
|
882
960
|
|
|
883
961
|
return true
|
|
884
962
|
}
|
|
@@ -886,7 +964,7 @@ export default class extends Controller {
|
|
|
886
964
|
return false
|
|
887
965
|
}
|
|
888
966
|
|
|
889
|
-
this
|
|
967
|
+
this.#markDefaultScrollPositionApplied()
|
|
890
968
|
|
|
891
969
|
if (this.#scrollToElement(element, options)) {
|
|
892
970
|
this.pendingScrollToMessage = null
|
|
@@ -910,7 +988,7 @@ export default class extends Controller {
|
|
|
910
988
|
if (!this.#scrollToElement(element, pending.options)) return false
|
|
911
989
|
|
|
912
990
|
this.pendingScrollToMessage = null
|
|
913
|
-
this
|
|
991
|
+
this.#markDefaultScrollPositionApplied()
|
|
914
992
|
|
|
915
993
|
return true
|
|
916
994
|
}
|
|
@@ -44,6 +44,7 @@ export default class NavigationMenuController extends Controller {
|
|
|
44
44
|
#cancelExit = new Map() // value -> abandon-this-panel's-exit (per panel, not global)
|
|
45
45
|
#onOutsidePress = null
|
|
46
46
|
#unsubscribeBeforeCache = null
|
|
47
|
+
#itemsObserver = null
|
|
47
48
|
#sizeGeneration = 0
|
|
48
49
|
|
|
49
50
|
/**
|
|
@@ -62,6 +63,16 @@ export default class NavigationMenuController extends Controller {
|
|
|
62
63
|
this.#close()
|
|
63
64
|
flushPendingExits()
|
|
64
65
|
})
|
|
66
|
+
|
|
67
|
+
// A morph or stream that removes the OPEN trigger's item takes the
|
|
68
|
+
// per-item panel with it but leaves an adopted viewport panel behind,
|
|
69
|
+
// and either way the bar still counts itself open: close as if the
|
|
70
|
+
// trigger had been dismissed, and drop any adopted panel no trigger
|
|
71
|
+
// owns any more.
|
|
72
|
+
if (typeof MutationObserver !== "undefined") {
|
|
73
|
+
this.#itemsObserver = new MutationObserver(() => this.#reconcileRemovedItems())
|
|
74
|
+
this.#itemsObserver.observe(this.element, { childList: true, subtree: true })
|
|
75
|
+
}
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
/**
|
|
@@ -73,6 +84,8 @@ export default class NavigationMenuController extends Controller {
|
|
|
73
84
|
this.#unbindOutsidePress()
|
|
74
85
|
this.#unsubscribeBeforeCache?.()
|
|
75
86
|
this.#unsubscribeBeforeCache = null
|
|
87
|
+
this.#itemsObserver?.disconnect()
|
|
88
|
+
this.#itemsObserver = null
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
/**
|
|
@@ -82,7 +95,7 @@ export default class NavigationMenuController extends Controller {
|
|
|
82
95
|
*/
|
|
83
96
|
toggle(event) {
|
|
84
97
|
const value = this.#valueFrom(event)
|
|
85
|
-
if (value === null) return
|
|
98
|
+
if (value === null || this.#disabled(value)) return
|
|
86
99
|
|
|
87
100
|
this.#clearTimer()
|
|
88
101
|
if (this.#openValue === value) this.#close()
|
|
@@ -101,7 +114,7 @@ export default class NavigationMenuController extends Controller {
|
|
|
101
114
|
if (event.pointerType === "touch") return // touch is click's job
|
|
102
115
|
|
|
103
116
|
const value = this.#valueFrom(event)
|
|
104
|
-
if (value === null) return
|
|
117
|
+
if (value === null || this.#disabled(value)) return
|
|
105
118
|
if (value === this.#openValue) {
|
|
106
119
|
this.#clearTimer() // re-entering the open item cancels a pending close
|
|
107
120
|
return
|
|
@@ -152,6 +165,20 @@ export default class NavigationMenuController extends Controller {
|
|
|
152
165
|
return
|
|
153
166
|
}
|
|
154
167
|
|
|
168
|
+
// ArrowDown on a trigger opens its panel and is consumed; focus stays
|
|
169
|
+
// on the trigger (Tab enters the panel). A top-level link is not a
|
|
170
|
+
// trigger and keeps the key.
|
|
171
|
+
if (event.key === "ArrowDown") {
|
|
172
|
+
const trigger = event.target instanceof Element ? event.target.closest(TRIGGER_SELECTOR) : null
|
|
173
|
+
const value = this.#valueFrom(event)
|
|
174
|
+
if (!trigger || value === null || this.#disabled(value)) return
|
|
175
|
+
|
|
176
|
+
event.preventDefault()
|
|
177
|
+
this.#clearTimer()
|
|
178
|
+
if (this.#openValue !== value) this.#open(value)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
155
182
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
|
|
156
183
|
|
|
157
184
|
const stops = this.#arrowStops()
|
|
@@ -211,6 +238,28 @@ export default class NavigationMenuController extends Controller {
|
|
|
211
238
|
this.#bindOutsidePress()
|
|
212
239
|
}
|
|
213
240
|
|
|
241
|
+
#reconcileRemovedItems() {
|
|
242
|
+
const viewport = this.#viewport()
|
|
243
|
+
if (viewport) {
|
|
244
|
+
for (const panel of viewport.querySelectorAll("[data-viewport-panel]")) {
|
|
245
|
+
if (!this.#ownerOf(panel)) panel.remove()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (this.#openValue === null || this.#triggerFor(this.#openValue)) return
|
|
249
|
+
|
|
250
|
+
this.#clearTimer()
|
|
251
|
+
this.#close()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// The trigger whose aria-controls names this adopted panel, if it still
|
|
255
|
+
// exists.
|
|
256
|
+
#ownerOf(panel) {
|
|
257
|
+
if (!panel.id) return null
|
|
258
|
+
|
|
259
|
+
return [...this.element.querySelectorAll(TRIGGER_SELECTOR)]
|
|
260
|
+
.find((trigger) => trigger.getAttribute("aria-controls") === panel.id) ?? null
|
|
261
|
+
}
|
|
262
|
+
|
|
214
263
|
#close() {
|
|
215
264
|
if (this.#openValue === null) return
|
|
216
265
|
|
|
@@ -434,6 +483,12 @@ export default class NavigationMenuController extends Controller {
|
|
|
434
483
|
return this.#itemFor(value)?.querySelector(TRIGGER_SELECTOR) ?? null
|
|
435
484
|
}
|
|
436
485
|
|
|
486
|
+
// A disabled trigger (native disabled, data-disabled) never opens its
|
|
487
|
+
// panel - not by click, not by hover - and the arrows step over it.
|
|
488
|
+
#disabled(value) {
|
|
489
|
+
return this.#triggerFor(value)?.disabled === true
|
|
490
|
+
}
|
|
491
|
+
|
|
437
492
|
// An adopted panel (viewport mode) no longer lives inside its item -
|
|
438
493
|
// the trigger's aria-controls id finds it wherever it moved.
|
|
439
494
|
#panelFor(value) {
|
|
@@ -449,10 +504,10 @@ export default class NavigationMenuController extends Controller {
|
|
|
449
504
|
.find((item) => item.dataset.value === value) ?? null
|
|
450
505
|
}
|
|
451
506
|
|
|
452
|
-
// Arrow stops: every trigger and top-level link in the bar, DOM order.
|
|
507
|
+
// Arrow stops: every enabled trigger and top-level link in the bar, DOM order.
|
|
453
508
|
#arrowStops() {
|
|
454
509
|
return [...this.element.querySelectorAll(
|
|
455
510
|
`${TRIGGER_SELECTOR}, [data-slot="navigation-menu-list"] > ${ITEM_SELECTOR} > a`
|
|
456
|
-
)]
|
|
511
|
+
)].filter((stop) => !stop.disabled)
|
|
457
512
|
}
|
|
458
513
|
}
|
|
@@ -32,7 +32,6 @@ export default class extends Controller {
|
|
|
32
32
|
static events = ["poetry:number-field:change", "poetry:number-field:commit"]
|
|
33
33
|
|
|
34
34
|
#value = null
|
|
35
|
-
#focusedOnce = false
|
|
36
35
|
#holdTimer = null
|
|
37
36
|
#holdInterval = null
|
|
38
37
|
#holdChanged = false
|
|
@@ -119,15 +118,6 @@ export default class extends Controller {
|
|
|
119
118
|
if (parsed !== null) this.#apply(parsed, { display: false })
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
/** The focus action: the first focus parks the caret at the end. */
|
|
123
|
-
focus() {
|
|
124
|
-
if (this.#focusedOnce) return
|
|
125
|
-
|
|
126
|
-
this.#focusedOnce = true
|
|
127
|
-
const end = this.inputTarget.value.length
|
|
128
|
-
this.inputTarget.setSelectionRange(end, end)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
121
|
/**
|
|
132
122
|
* The blur action - the text commit point: empty clears, unparseable
|
|
133
123
|
* text is left as typed with no commit, parseable text clamps and
|
|
@@ -161,7 +151,7 @@ export default class extends Controller {
|
|
|
161
151
|
if (event.button !== 0 || this.inputTarget.readOnly) return
|
|
162
152
|
|
|
163
153
|
// Mouse focuses the input (touch would pop the software keyboard).
|
|
164
|
-
if (event.pointerType !== "touch") this
|
|
154
|
+
if (event.pointerType !== "touch") this.#focusInput()
|
|
165
155
|
this.#pressTicked = true
|
|
166
156
|
this.#holdChanged = false
|
|
167
157
|
const direction = this.#directionFor(event.currentTarget)
|
|
@@ -224,6 +214,17 @@ export default class extends Controller {
|
|
|
224
214
|
return changed
|
|
225
215
|
}
|
|
226
216
|
|
|
217
|
+
// Programmatic focus (the steppers) parks the caret at the end BEFORE
|
|
218
|
+
// focusing: every engine restores the stored selection on focus, and a
|
|
219
|
+
// host's own focus handler can still choose differently. Keyboard and
|
|
220
|
+
// pointer focus keep the browser's native selection (Tab selects the
|
|
221
|
+
// value, a click places the caret).
|
|
222
|
+
#focusInput() {
|
|
223
|
+
const end = this.inputTarget.value.length
|
|
224
|
+
this.inputTarget.setSelectionRange(end, end)
|
|
225
|
+
this.inputTarget.focus()
|
|
226
|
+
}
|
|
227
|
+
|
|
227
228
|
#directionFor(button) {
|
|
228
229
|
return this.hasIncrementTarget && button === this.incrementTarget ? 1 : -1
|
|
229
230
|
}
|
|
@@ -240,13 +241,22 @@ export default class extends Controller {
|
|
|
240
241
|
this.#apply(candidate, { display: true })
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
// The dominant axis decides (a precision touchpad emits sub-pixel
|
|
245
|
+
// cross-axis noise): a sideways gesture is page scroll and passes
|
|
246
|
+
// through untouched, except under Shift, which some browsers deliver
|
|
247
|
+
// on the horizontal axis - there the horizontal delta is the intended
|
|
248
|
+
// one. Positive is "down", stepping the value down.
|
|
243
249
|
#wheel(event) {
|
|
244
250
|
if (event.ctrlKey || document.activeElement !== this.inputTarget) return
|
|
245
251
|
|
|
246
|
-
event.
|
|
247
|
-
if (event.
|
|
252
|
+
const horizontal = Math.abs(event.deltaX) > Math.abs(event.deltaY)
|
|
253
|
+
if (horizontal && !event.shiftKey) return
|
|
254
|
+
|
|
255
|
+
const delta = horizontal ? event.deltaX : event.deltaY
|
|
256
|
+
if (delta === 0) return
|
|
248
257
|
|
|
249
|
-
|
|
258
|
+
event.preventDefault()
|
|
259
|
+
this.#step(delta > 0 ? -1 : 1, event)
|
|
250
260
|
this.#commit()
|
|
251
261
|
}
|
|
252
262
|
|
|
@@ -595,9 +595,9 @@
|
|
|
595
595
|
"disconnect",
|
|
596
596
|
"focusOpen",
|
|
597
597
|
"openValueChanged",
|
|
598
|
+
"pointerDown",
|
|
598
599
|
"pointerEnter",
|
|
599
|
-
"pointerLeave"
|
|
600
|
-
"touchGuard"
|
|
600
|
+
"pointerLeave"
|
|
601
601
|
],
|
|
602
602
|
"events": [
|
|
603
603
|
"poetry:hover-card:closed",
|
|
@@ -841,7 +841,6 @@
|
|
|
841
841
|
"blur",
|
|
842
842
|
"connect",
|
|
843
843
|
"disconnect",
|
|
844
|
-
"focus",
|
|
845
844
|
"hiddenChanged",
|
|
846
845
|
"input",
|
|
847
846
|
"keydown",
|
data/lib/poetry/core/check.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Poetry
|
|
|
47
47
|
#
|
|
48
48
|
# @api private
|
|
49
49
|
class Catalog
|
|
50
|
-
PASSTHROUGH = %w[class id key webmcp data aria role style if unless].freeze
|
|
50
|
+
PASSTHROUGH = %w[class id key webmcp identity data aria role style if unless].freeze
|
|
51
51
|
COLOR_FAMILIES = %w[
|
|
52
52
|
slate gray zinc neutral stone red orange amber yellow lime green emerald
|
|
53
53
|
teal cyan sky blue indigo violet purple fuchsia pink rose
|
|
@@ -401,7 +401,7 @@ module Poetry
|
|
|
401
401
|
unless path
|
|
402
402
|
return helper_findings(helper, call, base_line) + yieldless_findings(helper, call, line) +
|
|
403
403
|
helper_arity_findings(helper, call, line) + webmcp_form_findings(helper, call, line) +
|
|
404
|
-
passthrough_findings(keyword_pairs(call), base_line) + data_findings(call, base_line)
|
|
404
|
+
passthrough_findings(keyword_pairs(call), base_line) + data_findings(call, base_line, helper)
|
|
405
405
|
end
|
|
406
406
|
|
|
407
407
|
record_binding(call, path, bindings, line)
|
|
@@ -415,7 +415,7 @@ module Poetry
|
|
|
415
415
|
content_findings(path, helper, call, line, content_fed) +
|
|
416
416
|
blockless_slot_findings(path, helper, call, pairs, line) +
|
|
417
417
|
requires_any_findings(path, call, pairs, line, content_fed) +
|
|
418
|
-
passthrough_findings(pairs, base_line) + data_findings(call, base_line)
|
|
418
|
+
passthrough_findings(pairs, base_line) + data_findings(call, base_line, helper)
|
|
419
419
|
end
|
|
420
420
|
|
|
421
421
|
# The requires_content tier: a component that raises without a
|
|
@@ -474,13 +474,19 @@ module Poetry
|
|
|
474
474
|
known = @catalog.option_names(path)
|
|
475
475
|
entry = @catalog.option_entry(path, key)
|
|
476
476
|
|
|
477
|
+
# The component's identity attribute: never overridable (the render
|
|
478
|
+
# raises in development and test, drops it in production).
|
|
479
|
+
return [reserved_finding("data-component", helper_of(path), line)] if key == "data-component"
|
|
480
|
+
|
|
477
481
|
unless known.include?(key) || Catalog::PASSTHROUGH.include?(key)
|
|
478
482
|
suggestion = suggest(key, known)
|
|
479
|
-
# No suggestion => an intentional pass-through html attribute, not a
|
|
483
|
+
# No suggestion => an intentional pass-through html attribute, not a
|
|
484
|
+
# typo. A suggestion is the render-time raise (development, test).
|
|
480
485
|
if suggestion
|
|
481
|
-
findings << Finding.new(rule: "unknown-option", severity: :
|
|
482
|
-
message: "#{helper_of(path)} has no option #{key}"
|
|
483
|
-
|
|
486
|
+
findings << Finding.new(rule: "unknown-option", severity: :error,
|
|
487
|
+
message: "#{helper_of(path)} has no option #{key} " \
|
|
488
|
+
"(unknown keywords render as HTML attributes)",
|
|
489
|
+
line: line, suggestion: suggestion)
|
|
484
490
|
end
|
|
485
491
|
end
|
|
486
492
|
|
|
@@ -640,7 +646,7 @@ module Poetry
|
|
|
640
646
|
findings = arity_findings(entry, slot_name, call, line) +
|
|
641
647
|
setter_block_findings(entry, slot_name, call, line) +
|
|
642
648
|
setter_keyword_findings(entry, slot_name, call, base_line) +
|
|
643
|
-
passthrough_findings(keyword_pairs(call), base_line) + data_findings(call, base_line)
|
|
649
|
+
passthrough_findings(keyword_pairs(call), base_line) + data_findings(call, base_line, label)
|
|
644
650
|
component = entry["component"]
|
|
645
651
|
return findings unless component
|
|
646
652
|
|
|
@@ -948,18 +954,29 @@ module Poetry
|
|
|
948
954
|
# data key (`poetry__core__x_target:` and `"poetry--core--x-target":`
|
|
949
955
|
# both render data-poetry--core--x-target), so the rendered name is
|
|
950
956
|
# what gets checked. Dynamic values are left alone.
|
|
951
|
-
def data_findings(call, base_line)
|
|
957
|
+
def data_findings(call, base_line, helper)
|
|
952
958
|
hash = hash_argument(call, "data")
|
|
953
959
|
return [] unless hash
|
|
954
960
|
|
|
955
961
|
hash_pairs(hash).flat_map do |key, node|
|
|
962
|
+
line = base_line + node.location.start_line - 1
|
|
963
|
+
next [reserved_finding("data: { component: }", helper, line)] if key == "component"
|
|
964
|
+
|
|
956
965
|
value = literal_value(node)
|
|
957
966
|
next [] unless value.is_a?(String)
|
|
958
967
|
|
|
959
|
-
stimulus_findings("data-#{key.tr("_", "-")}", value,
|
|
968
|
+
stimulus_findings("data-#{key.tr("_", "-")}", value, line)
|
|
960
969
|
end
|
|
961
970
|
end
|
|
962
971
|
|
|
972
|
+
def reserved_finding(spelling, helper, line)
|
|
973
|
+
Finding.new(rule: "reserved-attribute", severity: :error,
|
|
974
|
+
message: "#{spelling} is #{helper}'s own identity attribute and is never overridable " \
|
|
975
|
+
"(a composed root takes identity: instead; the raw attribute raises at render " \
|
|
976
|
+
"in development)",
|
|
977
|
+
line: line)
|
|
978
|
+
end
|
|
979
|
+
|
|
963
980
|
def controller_findings(value, line)
|
|
964
981
|
value.split.filter_map do |identifier|
|
|
965
982
|
next unless identifier.start_with?(POETRY_PREFIX)
|
data/lib/poetry/core/config.rb
CHANGED
|
@@ -56,7 +56,8 @@ module Poetry
|
|
|
56
56
|
# actions.
|
|
57
57
|
# - `css_mode` (`:tailwind`) - `:tailwind` emits resolved utility
|
|
58
58
|
# classes; `:bem` emits the BEM token IR for bring-your-own-CSS
|
|
59
|
-
#
|
|
59
|
+
# kits authored on this DSL (poetry-ui is Tailwind-native and is
|
|
60
|
+
# not a `:bem` consumer).
|
|
60
61
|
# - `icon_library` (`:lucide`) - the active icon set, by the key it
|
|
61
62
|
# registered under ({Poetry::Core::Icons.register}).
|
|
62
63
|
# - `raise_on_missing_icon` (`nil`) - the policy for a dynamic icon
|
|
@@ -169,7 +170,9 @@ module Poetry
|
|
|
169
170
|
#
|
|
170
171
|
# @!method css_mode
|
|
171
172
|
# The class emission mode: `:tailwind` resolves style values to
|
|
172
|
-
# utility classes, `:bem` emits the BEM token IR
|
|
173
|
+
# utility classes, `:bem` emits the BEM token IR - for kits authored
|
|
174
|
+
# on the DSL that write their own templates; poetry-ui is
|
|
175
|
+
# Tailwind-native and is not a `:bem` consumer.
|
|
173
176
|
# @return [Symbol] :tailwind or :bem
|
|
174
177
|
#
|
|
175
178
|
# @!method css_mode=(mode)
|
data/lib/poetry/core/version.rb
CHANGED