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,265 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { directionOf } from "@poetry/controllers/helpers/direction"
|
|
3
|
+
import { createTypeahead, typeaheadLabel } from "@poetry/controllers/helpers/typeahead"
|
|
4
|
+
|
|
5
|
+
// The Tree engine (the flat-treegrid contract): the
|
|
6
|
+
// server renders a FLAT list of role=row siblings - hierarchy lives
|
|
7
|
+
// entirely in aria-level/posinset/setsize (static per render) - so this
|
|
8
|
+
// controller owns only what HTML cannot: roving focus over VISIBLE rows,
|
|
9
|
+
// the four-branch ArrowLeft/Right expansion logic (including
|
|
10
|
+
// focus-to-parent), Enter-toggles-expandable (the no-action default),
|
|
11
|
+
// typeahead, and subtree show/hide that preserves nested collapsed state
|
|
12
|
+
// (a row is visible iff every ancestor is expanded).
|
|
13
|
+
//
|
|
14
|
+
// Expansion state IS the DOM (aria-expanded + hidden); the host persists
|
|
15
|
+
// it by listening for poetry:tree:toggle. Selection modes are deferred
|
|
16
|
+
// (the TagGroup reasoning).
|
|
17
|
+
const EVENT_PREFIX = "poetry:tree"
|
|
18
|
+
|
|
19
|
+
export default class TreeController extends Controller {
|
|
20
|
+
// The events this controller dispatches (manifest surface;
|
|
21
|
+
// events_declaration.test.js enforces the list stays honest).
|
|
22
|
+
static events = ["poetry:tree:toggle"]
|
|
23
|
+
|
|
24
|
+
#typeahead = createTypeahead()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Applies ancestor-expansion visibility and settles the single tab
|
|
28
|
+
* stop.
|
|
29
|
+
*/
|
|
30
|
+
connect() {
|
|
31
|
+
this.#applyVisibility()
|
|
32
|
+
this.#settleTabstop()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The treegrid container's keydown action (row-origin keys only):
|
|
37
|
+
* arrows rove the visible rows, the direction-aware expand/collapse
|
|
38
|
+
* pair runs the four-branch logic (collapse on a leaf focuses the
|
|
39
|
+
* parent), Home/End jump the edges, Enter toggles expandables, and
|
|
40
|
+
* printable keys run typeahead.
|
|
41
|
+
*
|
|
42
|
+
* @param {KeyboardEvent} event
|
|
43
|
+
*/
|
|
44
|
+
keydown(event) {
|
|
45
|
+
const row = event.target.closest("[data-slot='tree-item']")
|
|
46
|
+
|
|
47
|
+
// Row-origin keys only (a control inside a row keeps its own keys).
|
|
48
|
+
if (!row || event.target !== row) return
|
|
49
|
+
if (event.isComposing) return
|
|
50
|
+
|
|
51
|
+
const [expandKey, collapseKey] =
|
|
52
|
+
directionOf(row) === "rtl" ? ["ArrowLeft", "ArrowRight"] : ["ArrowRight", "ArrowLeft"]
|
|
53
|
+
|
|
54
|
+
switch (event.key) {
|
|
55
|
+
case "ArrowDown": event.preventDefault(); this.#move(row, 1); break
|
|
56
|
+
case "ArrowUp": event.preventDefault(); this.#move(row, -1); break
|
|
57
|
+
case "Home": event.preventDefault(); this.#focusRow(this.#visibleRows()[0]); break
|
|
58
|
+
case "End": event.preventDefault(); this.#focusRow(this.#visibleRows().at(-1)); break
|
|
59
|
+
case expandKey: this.#expandKey(event, row); break
|
|
60
|
+
case collapseKey: this.#collapseKey(event, row); break
|
|
61
|
+
case "Enter": this.#enterKey(event, row); break
|
|
62
|
+
case " ": if (this.#typeahead.pending()) { event.preventDefault(); this.#search(" ", row) } break
|
|
63
|
+
default: this.#maybeTypeahead(event, row)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Each row's click action: toggles when expandable (the default press
|
|
69
|
+
* behavior with no action/link/selection); clicks on inner controls
|
|
70
|
+
* (links, the chevron) stay their own.
|
|
71
|
+
*
|
|
72
|
+
* @param {MouseEvent} event
|
|
73
|
+
*/
|
|
74
|
+
press(event) {
|
|
75
|
+
const row = event.target.closest("[data-slot='tree-item']")
|
|
76
|
+
|
|
77
|
+
if (!row || row.hasAttribute("data-disabled")) return
|
|
78
|
+
if (event.target.closest("a, button, input, select, textarea")) return
|
|
79
|
+
if (!this.#expandable(row)) return
|
|
80
|
+
|
|
81
|
+
this.#toggle(row)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The chevron's click action (the chevron rides tabindex -1; its
|
|
86
|
+
* pointerdown is preventDefault'd via pressStart so focus never leaves
|
|
87
|
+
* the row): toggles, then refocuses the row.
|
|
88
|
+
*
|
|
89
|
+
* @param {MouseEvent} event
|
|
90
|
+
*/
|
|
91
|
+
toggle(event) {
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
event.stopPropagation()
|
|
94
|
+
const row = event.target.closest("[data-slot='tree-item']")
|
|
95
|
+
|
|
96
|
+
if (!row || row.hasAttribute("data-disabled")) return
|
|
97
|
+
|
|
98
|
+
this.#toggle(row)
|
|
99
|
+
this.#focusRow(row)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The chevron's pointerdown action: never steal focus from the row.
|
|
104
|
+
*
|
|
105
|
+
* @param {PointerEvent} event
|
|
106
|
+
*/
|
|
107
|
+
pressStart(event) {
|
|
108
|
+
event.preventDefault()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- expansion ---
|
|
112
|
+
|
|
113
|
+
#expandKey(event, row) {
|
|
114
|
+
if (!this.#expandable(row)) return
|
|
115
|
+
|
|
116
|
+
if (row.getAttribute("aria-expanded") === "false") {
|
|
117
|
+
event.preventDefault()
|
|
118
|
+
this.#toggle(row)
|
|
119
|
+
}
|
|
120
|
+
// Expanded parent / leaf: no-op v1 (focus-into-row-children deferred).
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#collapseKey(event, row) {
|
|
124
|
+
if (this.#expandable(row) && row.getAttribute("aria-expanded") === "true") {
|
|
125
|
+
event.preventDefault()
|
|
126
|
+
this.#toggle(row)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Leaf or collapsed: focus the PARENT row (the nearest preceding row
|
|
131
|
+
// with a smaller level).
|
|
132
|
+
const parent = this.#parentOf(row)
|
|
133
|
+
|
|
134
|
+
if (parent) {
|
|
135
|
+
event.preventDefault()
|
|
136
|
+
this.#focusRow(parent)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#enterKey(event, row) {
|
|
141
|
+
if (row.hasAttribute("data-disabled") || !this.#expandable(row)) return
|
|
142
|
+
|
|
143
|
+
event.preventDefault()
|
|
144
|
+
this.#toggle(row)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#toggle(row) {
|
|
148
|
+
const expanded = row.getAttribute("aria-expanded") !== "true"
|
|
149
|
+
|
|
150
|
+
row.setAttribute("aria-expanded", String(expanded))
|
|
151
|
+
row.toggleAttribute("data-expanded", expanded)
|
|
152
|
+
|
|
153
|
+
// The chevron's name flips with the state (Expand/Collapse; the
|
|
154
|
+
// strings arrive server-localized as data attributes).
|
|
155
|
+
const chevron = row.querySelector("[data-slot='tree-item-toggle']")
|
|
156
|
+
const label = chevron?.getAttribute(expanded ? "data-collapse-label" : "data-expand-label")
|
|
157
|
+
|
|
158
|
+
if (chevron && label) chevron.setAttribute("aria-label", label)
|
|
159
|
+
|
|
160
|
+
this.#applyVisibility()
|
|
161
|
+
|
|
162
|
+
this.dispatch("toggle", {
|
|
163
|
+
prefix: EVENT_PREFIX,
|
|
164
|
+
detail: { id: row.id, value: row.getAttribute("data-value"), expanded }
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// A row is visible iff EVERY ancestor is expanded: walk the flat list
|
|
169
|
+
// tracking the shallowest collapsed level - rows deeper than it hide.
|
|
170
|
+
#applyVisibility() {
|
|
171
|
+
let hideBelow = Infinity
|
|
172
|
+
|
|
173
|
+
for (const row of this.#rows()) {
|
|
174
|
+
const level = this.#levelOf(row)
|
|
175
|
+
|
|
176
|
+
if (level > hideBelow) {
|
|
177
|
+
row.hidden = true
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
row.hidden = false
|
|
182
|
+
hideBelow = this.#expandable(row) && row.getAttribute("aria-expanded") !== "true"
|
|
183
|
+
? level
|
|
184
|
+
: Infinity
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.#settleTabstop()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- roving focus over visible rows ---
|
|
191
|
+
|
|
192
|
+
#move(row, direction) {
|
|
193
|
+
const rows = this.#visibleRows()
|
|
194
|
+
const next = rows[rows.indexOf(row) + direction]
|
|
195
|
+
|
|
196
|
+
this.#focusRow(next)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#focusRow(row) {
|
|
200
|
+
if (!row) return
|
|
201
|
+
|
|
202
|
+
for (const candidate of this.#rows()) {
|
|
203
|
+
candidate.setAttribute("tabindex", candidate === row ? "0" : "-1")
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
row.focus()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Exactly one visible row keeps tabindex 0 (the focused one if it is
|
|
210
|
+
// still visible, else the first visible row).
|
|
211
|
+
#settleTabstop() {
|
|
212
|
+
const rows = this.#visibleRows()
|
|
213
|
+
|
|
214
|
+
if (rows.length === 0) return
|
|
215
|
+
|
|
216
|
+
const current = rows.find((row) => row.getAttribute("tabindex") === "0") ?? rows[0]
|
|
217
|
+
|
|
218
|
+
for (const row of this.#rows()) {
|
|
219
|
+
row.setAttribute("tabindex", row === current ? "0" : "-1")
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#maybeTypeahead(event, row) {
|
|
224
|
+
if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) return
|
|
225
|
+
|
|
226
|
+
event.preventDefault()
|
|
227
|
+
this.#search(event.key, row)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#search(key, row) {
|
|
231
|
+
const rows = this.#visibleRows().filter((candidate) => !candidate.hasAttribute("data-disabled"))
|
|
232
|
+
const match = this.#typeahead.search(key, rows, { active: row, labelOf: typeaheadLabel })
|
|
233
|
+
|
|
234
|
+
if (match) this.#focusRow(match)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- the flat-row math ---
|
|
238
|
+
|
|
239
|
+
#rows() {
|
|
240
|
+
return Array.from(this.element.querySelectorAll("[data-slot='tree-item']"))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#visibleRows() {
|
|
244
|
+
return this.#rows().filter((row) => !row.hidden)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#levelOf(row) {
|
|
248
|
+
return Number(row.getAttribute("aria-level")) || 1
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#expandable(row) {
|
|
252
|
+
return row.hasAttribute("aria-expanded")
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#parentOf(row) {
|
|
256
|
+
const rows = this.#rows()
|
|
257
|
+
const level = this.#levelOf(row)
|
|
258
|
+
|
|
259
|
+
for (let index = rows.indexOf(row) - 1; index >= 0; index -= 1) {
|
|
260
|
+
if (this.#levelOf(rows[index]) < level) return rows[index]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
}
|