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,197 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { acquire, release } from "@poetry/controllers/helpers/announce"
|
|
3
|
+
import { isEditingTarget, matchesHotkey } from "@poetry/controllers/helpers/hotkey"
|
|
4
|
+
import { stateOf } from "@poetry/controllers/helpers/state"
|
|
5
|
+
|
|
6
|
+
// The toast viewport (one per page): the labeled role=region <ol> that is
|
|
7
|
+
// the Turbo Stream append target (id=poetry-toaster, data-turbo-permanent).
|
|
8
|
+
// It ACQUIRES the announce singleton for its lifetime (items announce
|
|
9
|
+
// through it on connect), owns the F8 hotkey (focus moves to the most
|
|
10
|
+
// recent toast; the prior focus is remembered so dismissal returns it),
|
|
11
|
+
// enforces the visible LIMIT (default 3 - the oldest overflow toasts queue
|
|
12
|
+
// hidden with their timers held, promoted as newer ones dismiss), and owns
|
|
13
|
+
// the stack reflow (--poetry-toast-index, newest = 0, for the offset/scale
|
|
14
|
+
// stack styling).
|
|
15
|
+
//
|
|
16
|
+
// Items arrive by server render, Turbo Stream append, or a stamp (the
|
|
17
|
+
// poetry:toaster:stamp window event - poetry_toast_trigger's no-round-trip
|
|
18
|
+
// path, cloning a <template> toast into the region);
|
|
19
|
+
// a childList MutationObserver reconciles limit + reflow on every change,
|
|
20
|
+
// so no append path needs to know about the toaster.
|
|
21
|
+
const TOAST_SELECTOR = '[data-slot="toast"]'
|
|
22
|
+
const TOAST_IDENTIFIER = "poetry--core--toast"
|
|
23
|
+
|
|
24
|
+
const INDEX_PROPERTY = "--poetry-toast-index"
|
|
25
|
+
|
|
26
|
+
export default class ToasterController extends Controller {
|
|
27
|
+
static targets = ["item"]
|
|
28
|
+
static values = {
|
|
29
|
+
hotkey: { type: String, default: "F8" },
|
|
30
|
+
limit: { type: Number, default: 3 },
|
|
31
|
+
position: String
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#observer = null
|
|
35
|
+
#returnFocusTo = null
|
|
36
|
+
#onKeydown = (event) => this.focusRegion(event)
|
|
37
|
+
#onDismiss = (event) => this.#handleDismiss(event)
|
|
38
|
+
#onStamp = (event) => this.#stamp(event)
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Acquires the announce singleton, wires the hotkey / stamp / dismiss
|
|
42
|
+
* listeners, and starts the childList reconciler.
|
|
43
|
+
*/
|
|
44
|
+
connect() {
|
|
45
|
+
acquire() // the singleton lives while a toaster is connected
|
|
46
|
+
|
|
47
|
+
window.addEventListener("keydown", this.#onKeydown)
|
|
48
|
+
window.addEventListener("poetry:toaster:stamp", this.#onStamp)
|
|
49
|
+
this.element.addEventListener("poetry:toast:dismiss", this.#onDismiss)
|
|
50
|
+
|
|
51
|
+
this.#observer = new MutationObserver(() => this.#reconcile())
|
|
52
|
+
this.#observer.observe(this.element, { childList: true })
|
|
53
|
+
|
|
54
|
+
this.#reconcile()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Unwires everything and releases the announce singleton. */
|
|
58
|
+
disconnect() {
|
|
59
|
+
this.#observer?.disconnect()
|
|
60
|
+
this.#observer = null
|
|
61
|
+
window.removeEventListener("keydown", this.#onKeydown)
|
|
62
|
+
window.removeEventListener("poetry:toaster:stamp", this.#onStamp)
|
|
63
|
+
this.element.removeEventListener("poetry:toast:dismiss", this.#onDismiss)
|
|
64
|
+
this.#returnFocusTo = null
|
|
65
|
+
release()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The stamp delivery path: clone a <template>'s toast into the region.
|
|
69
|
+
// detail.toaster (an id) scopes multi-toaster pages - a stamp addressed
|
|
70
|
+
// elsewhere is ignored; unaddressed stamps land in every toaster that
|
|
71
|
+
// hears them (one per page canonically). The childList observer then
|
|
72
|
+
// reconciles limit + reflow like any other append.
|
|
73
|
+
#stamp(event) {
|
|
74
|
+
const { template, toaster } = event.detail || {}
|
|
75
|
+
if (toaster && toaster !== this.element.id) return
|
|
76
|
+
|
|
77
|
+
const node = template ? document.getElementById(template) : null
|
|
78
|
+
if (!(node instanceof HTMLTemplateElement)) {
|
|
79
|
+
console.warn(`poetry toaster: stamp template ${JSON.stringify(template)} not found or not a <template>`)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.element.append(node.content.cloneNode(true))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The hotkey's landing (F8 default, configurable): moves focus into the
|
|
88
|
+
* region - onto the most recent visible toast, else the region itself.
|
|
89
|
+
* The prior focus is remembered for the dismiss return.
|
|
90
|
+
*
|
|
91
|
+
* @param {Event} [event] - the window keydown when the hotkey drove it;
|
|
92
|
+
* programmatic calls may omit it
|
|
93
|
+
*/
|
|
94
|
+
focusRegion(event) {
|
|
95
|
+
if (event instanceof KeyboardEvent) {
|
|
96
|
+
// The full descriptor grammar + the editing-target rule (the hotkey
|
|
97
|
+
// controller's idiom): a bare key comparison fires on stray-modifier
|
|
98
|
+
// chords and while the user types in a field.
|
|
99
|
+
if (event.defaultPrevented || !matchesHotkey(event, this.hotkeyValue)) return
|
|
100
|
+
if (isEditingTarget(event) && !(event.metaKey || event.ctrlKey || event.altKey)) return
|
|
101
|
+
|
|
102
|
+
event.preventDefault()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const active = document.activeElement
|
|
106
|
+
|
|
107
|
+
if (active instanceof HTMLElement && !this.element.contains(active)) {
|
|
108
|
+
this.#returnFocusTo = active
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const target = this.#visibleToasts().at(-1) ?? this.element
|
|
112
|
+
|
|
113
|
+
target.focus()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Stack reflow: newest toast = index 0 (the front of the stack),
|
|
118
|
+
* written as the offset/scale custom property.
|
|
119
|
+
*/
|
|
120
|
+
reflow() {
|
|
121
|
+
const visible = this.#visibleToasts()
|
|
122
|
+
|
|
123
|
+
visible.forEach((toast, index) => {
|
|
124
|
+
toast.style.setProperty(INDEX_PROPERTY, String(visible.length - 1 - index))
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- internals ---
|
|
129
|
+
|
|
130
|
+
// Limit + queue + reflow, run on every childList change: the newest
|
|
131
|
+
// `limit` open toasts are visible; older overflow queues hidden (timer
|
|
132
|
+
// held via the toast controller's "queued" pause) until slots free up.
|
|
133
|
+
#reconcile() {
|
|
134
|
+
const open = this.#openToasts()
|
|
135
|
+
const overflow = Math.max(0, open.length - this.limitValue)
|
|
136
|
+
|
|
137
|
+
open.forEach((toast, index) => {
|
|
138
|
+
if (index < overflow) this.#queue(toast)
|
|
139
|
+
else this.#promote(toast)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
this.reflow()
|
|
143
|
+
this.#restoreFocusIfLost()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#queue(toast) {
|
|
147
|
+
if (toast.hasAttribute("data-queued")) return
|
|
148
|
+
|
|
149
|
+
toast.setAttribute("data-queued", "")
|
|
150
|
+
toast.hidden = true
|
|
151
|
+
this.#toastController(toast)?.pause("queued")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#promote(toast) {
|
|
155
|
+
if (!toast.hasAttribute("data-queued")) return
|
|
156
|
+
|
|
157
|
+
toast.removeAttribute("data-queued")
|
|
158
|
+
toast.hidden = false
|
|
159
|
+
this.#toastController(toast)?.resume("queued")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Dismissal focus return: when the dismissed toast held focus (hotkey
|
|
163
|
+
// entry), focus goes back to the remembered prior element.
|
|
164
|
+
#handleDismiss(event) {
|
|
165
|
+
const toast = event.target instanceof Element ? event.target.closest(TOAST_SELECTOR) : null
|
|
166
|
+
|
|
167
|
+
if (toast?.contains(document.activeElement) && this.#returnFocusTo?.isConnected) {
|
|
168
|
+
this.#returnFocusTo.focus()
|
|
169
|
+
this.#returnFocusTo = null
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A removed toast that carried focus drops it on <body>; hand it back.
|
|
174
|
+
#restoreFocusIfLost() {
|
|
175
|
+
if (document.activeElement !== document.body) return
|
|
176
|
+
if (!this.#returnFocusTo?.isConnected) return
|
|
177
|
+
|
|
178
|
+
this.#returnFocusTo.focus()
|
|
179
|
+
this.#returnFocusTo = null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#toasts() {
|
|
183
|
+
return Array.from(this.element.querySelectorAll(TOAST_SELECTOR))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#openToasts() {
|
|
187
|
+
return this.#toasts().filter((toast) => stateOf(toast) !== "closed")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#visibleToasts() {
|
|
191
|
+
return this.#openToasts().filter((toast) => !toast.hidden)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#toastController(toast) {
|
|
195
|
+
return this.application.getControllerForElementAndIdentifier(toast, TOAST_IDENTIFIER)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { setState, stateOf } from "@poetry/controllers/helpers/state"
|
|
3
|
+
|
|
4
|
+
// The ToggleGroup value-set machine (the Accordion composition, second
|
|
5
|
+
// consumer): this controller owns ONLY the pressed-values set + attribute
|
|
6
|
+
// writes; the shared poetry--core--roving-focus on the same root owns the
|
|
7
|
+
// keyboard (default tabindex-managing mode - one Tab stop). Items are DUMB
|
|
8
|
+
// buttons (data-action -> group#toggle; poetry--core--pressed is NOT
|
|
9
|
+
// attached in group context - one owner, no event soup).
|
|
10
|
+
//
|
|
11
|
+
// The role/vocabulary split, enforced here: type=single is
|
|
12
|
+
// RADIO semantics - items carry aria-checked (aria-pressed stripped) and
|
|
13
|
+
// re-pressing the sole pressed item deselects to EMPTY;
|
|
14
|
+
// type=multiple is toolbar semantics - independent aria-pressed items. The
|
|
15
|
+
// controller reads type once and never mixes vocabularies; the bare
|
|
16
|
+
// data-pressed presence boolean styles both types identically (Toggle's
|
|
17
|
+
// classes just work).
|
|
18
|
+
//
|
|
19
|
+
// After every transition the PRESSED item becomes the roving tab stop
|
|
20
|
+
// (re-entering the group lands on the selection) -
|
|
21
|
+
// written directly as the tabindex stamp roving-focus adopts.
|
|
22
|
+
const ITEM_SELECTOR = '[data-slot="toggle-group-item"]'
|
|
23
|
+
|
|
24
|
+
export default class ToggleGroupController extends Controller {
|
|
25
|
+
// The events this controller dispatches (manifest surface;
|
|
26
|
+
// events_declaration.test.js enforces the list stays honest).
|
|
27
|
+
static events = ["poetry:toggle-group:change"]
|
|
28
|
+
|
|
29
|
+
static values = {
|
|
30
|
+
type: { type: String, default: "single" }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reconcile-on-connect: re-derives the type-correct aria vocabulary
|
|
35
|
+
* from the server-rendered data-pressed truth, then prefers the pressed
|
|
36
|
+
* tab stop.
|
|
37
|
+
*/
|
|
38
|
+
connect() {
|
|
39
|
+
// Reconcile-on-connect: server-rendered data-pressed is the truth; the
|
|
40
|
+
// type-correct aria attribute is (re)derived from it, so a Turbo Stream
|
|
41
|
+
// re-render (or a hand-patched item) can never mix vocabularies.
|
|
42
|
+
for (const item of this.#items()) this.#write(item, stateOf(item) === "pressed")
|
|
43
|
+
|
|
44
|
+
this.#preferPressedTabStop()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- item activation (click / Space / Enter via native button click) ---
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Each item's click action (Space/Enter arrive via native button
|
|
51
|
+
* activation): single mode is radio semantics with deselect-to-empty;
|
|
52
|
+
* multiple XORs the value in and out.
|
|
53
|
+
*
|
|
54
|
+
* @param {MouseEvent} event
|
|
55
|
+
*/
|
|
56
|
+
toggle(event) {
|
|
57
|
+
const origin = event.currentTarget instanceof Element ? event.currentTarget : event.target
|
|
58
|
+
const item = origin instanceof Element ? origin.closest(ITEM_SELECTOR) : null
|
|
59
|
+
|
|
60
|
+
if (!item || this.#disabled(item)) return
|
|
61
|
+
|
|
62
|
+
const value = item.dataset.value
|
|
63
|
+
|
|
64
|
+
if (this.#single()) this.#toggleSingle(value)
|
|
65
|
+
else this.#toggleMultiple(value)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- the programmatic controllable-state surface ---
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The programmatic controllable-state surface: setValue("b") in single
|
|
72
|
+
* mode ("" / null clears); setValue(["a", "b"]) in multiple mode.
|
|
73
|
+
* Validated against type; unknown values are ignored (logged in dev -
|
|
74
|
+
* the contract's guard).
|
|
75
|
+
*
|
|
76
|
+
* @param {string | string[] | null} value
|
|
77
|
+
*/
|
|
78
|
+
setValue(value) {
|
|
79
|
+
const values = this.#known(Array.isArray(value) ? value : (value ? [value] : []))
|
|
80
|
+
|
|
81
|
+
if (this.#single()) {
|
|
82
|
+
if (Array.isArray(value) && value.length > 1) {
|
|
83
|
+
console.warn("poetry--core--toggle-group: setValue(array) is invalid with type=single")
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.#apply(new Set(values.slice(0, 1)))
|
|
88
|
+
} else {
|
|
89
|
+
if (!Array.isArray(value) && value != null && value !== "") {
|
|
90
|
+
console.warn("poetry--core--toggle-group: setValue expects an array with type=multiple")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.#apply(new Set(values))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- the set machine ---
|
|
99
|
+
|
|
100
|
+
// single: S == {v} -> {} (deselect-to-empty, contractual), else -> {v}.
|
|
101
|
+
#toggleSingle(value) {
|
|
102
|
+
const pressed = this.#pressedValues()
|
|
103
|
+
|
|
104
|
+
this.#apply(new Set(pressed.has(value) ? [] : [value]))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// multiple: S -> S XOR {v}.
|
|
108
|
+
#toggleMultiple(value) {
|
|
109
|
+
const pressed = this.#pressedValues()
|
|
110
|
+
|
|
111
|
+
if (pressed.has(value)) pressed.delete(value)
|
|
112
|
+
else pressed.add(value)
|
|
113
|
+
|
|
114
|
+
this.#apply(pressed)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#apply(next) {
|
|
118
|
+
const pressed = []
|
|
119
|
+
const unpressed = []
|
|
120
|
+
|
|
121
|
+
for (const item of this.#items()) {
|
|
122
|
+
const on = next.has(item.dataset.value)
|
|
123
|
+
const was = stateOf(item) === "pressed"
|
|
124
|
+
|
|
125
|
+
this.#write(item, on)
|
|
126
|
+
|
|
127
|
+
if (on && !was) pressed.push(item.dataset.value)
|
|
128
|
+
if (!on && was) unpressed.push(item.dataset.value)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (pressed.length === 0 && unpressed.length === 0) return
|
|
132
|
+
|
|
133
|
+
this.#preferPressedTabStop()
|
|
134
|
+
|
|
135
|
+
const values = [...next]
|
|
136
|
+
|
|
137
|
+
this.dispatch("change", {
|
|
138
|
+
prefix: "poetry:toggle-group",
|
|
139
|
+
detail: {
|
|
140
|
+
type: this.typeValue,
|
|
141
|
+
...(this.#single() ? { value: values[0] ?? null } : { values }),
|
|
142
|
+
pressed,
|
|
143
|
+
unpressed
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// data-pressed and the TYPE-CORRECT aria attribute, written together:
|
|
149
|
+
// single items are radios (aria-checked only - a stale aria-pressed
|
|
150
|
+
// would double-voice the state); multiple items are toggle buttons.
|
|
151
|
+
#write(item, on) {
|
|
152
|
+
setState(item, on ? "pressed" : "unpressed")
|
|
153
|
+
|
|
154
|
+
if (this.#single()) {
|
|
155
|
+
item.setAttribute("aria-checked", String(on))
|
|
156
|
+
item.removeAttribute("aria-pressed")
|
|
157
|
+
} else {
|
|
158
|
+
item.setAttribute("aria-pressed", String(on))
|
|
159
|
+
item.removeAttribute("aria-checked")
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The pressed item is the preferred tab stop.
|
|
164
|
+
// Written as the tabindex stamp the roving-focus controller adopts (its
|
|
165
|
+
// current-stop scan reads tabindex="0"); only when roving already manages
|
|
166
|
+
// tabindex here (the stamp exists), so a manage-tabindex:false host is
|
|
167
|
+
// left untouched.
|
|
168
|
+
#preferPressedTabStop() {
|
|
169
|
+
const items = this.#items()
|
|
170
|
+
|
|
171
|
+
if (!items.some((item) => item.hasAttribute("tabindex"))) return
|
|
172
|
+
|
|
173
|
+
const target = items.find((item) => stateOf(item) === "pressed" && !this.#disabled(item)) ??
|
|
174
|
+
items.find((item) => !this.#disabled(item))
|
|
175
|
+
|
|
176
|
+
if (!target) return
|
|
177
|
+
|
|
178
|
+
for (const item of items) item.setAttribute("tabindex", item === target ? "0" : "-1")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#pressedValues() {
|
|
182
|
+
return new Set(this.#items().filter((item) => stateOf(item) === "pressed").map((item) => item.dataset.value))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#known(values) {
|
|
186
|
+
const valid = new Set(this.#items().map((item) => item.dataset.value))
|
|
187
|
+
const unknown = values.filter((value) => !valid.has(value))
|
|
188
|
+
|
|
189
|
+
if (unknown.length > 0) console.debug(`poetry--core--toggle-group: unknown value(s) ${unknown.join(", ")}`)
|
|
190
|
+
|
|
191
|
+
return values.filter((value) => valid.has(value))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#items() {
|
|
195
|
+
return Array.from(this.element.querySelectorAll(ITEM_SELECTOR))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#single() {
|
|
199
|
+
return this.typeValue !== "multiple"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#disabled(item) {
|
|
203
|
+
return item.hasAttribute("disabled") || item.hasAttribute("data-disabled") ||
|
|
204
|
+
this.element.hasAttribute("data-disabled")
|
|
205
|
+
}
|
|
206
|
+
}
|