lostcoords-ui 0.4.1 → 0.6.0
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/README.md +364 -96
- data/app/assets/tailwind/lostcoords/ui/avatar.css +6 -6
- data/app/assets/tailwind/lostcoords/ui/base.css +45 -0
- data/app/assets/tailwind/lostcoords/ui/card.css +23 -22
- data/app/assets/tailwind/lostcoords/ui/choice.css +39 -0
- data/app/assets/tailwind/lostcoords/ui/code-block.css +10 -0
- data/app/assets/tailwind/lostcoords/ui/{disclosure.css → collapsible.css} +6 -6
- data/app/assets/tailwind/lostcoords/ui/components/code-block.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/collapsible.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/dialog.css +9 -0
- data/app/assets/tailwind/lostcoords/ui/components/emoji-picker.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/menu.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/prose.css +3 -0
- data/app/assets/tailwind/lostcoords/ui/components/table.css +3 -0
- data/app/assets/tailwind/lostcoords/ui/components/toast.css +5 -0
- data/app/assets/tailwind/lostcoords/ui/components/tooltip.css +4 -0
- data/app/assets/tailwind/lostcoords/ui/components.css +12 -0
- data/app/assets/tailwind/lostcoords/ui/copy-button.css +9 -9
- data/app/assets/tailwind/lostcoords/ui/core.css +21 -0
- data/app/assets/tailwind/lostcoords/ui/dialog.css +17 -17
- data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +86 -88
- data/app/assets/tailwind/lostcoords/ui/field.css +2 -2
- data/app/assets/tailwind/lostcoords/ui/glass.css +9 -8
- data/app/assets/tailwind/lostcoords/ui/layout.css +2 -2
- data/app/assets/tailwind/lostcoords/ui/popover.css +14 -14
- data/app/assets/tailwind/lostcoords/ui/prose.css +66 -0
- data/app/assets/tailwind/lostcoords/ui/sheet.css +62 -63
- data/app/assets/tailwind/lostcoords/ui/squircle.css +7 -27
- data/app/assets/tailwind/lostcoords/ui/table.css +51 -0
- data/app/assets/tailwind/lostcoords/ui/theme.css +209 -225
- data/app/assets/tailwind/lostcoords/ui/toast.css +23 -0
- data/app/assets/tailwind/lostcoords/ui/tooltip.css +51 -0
- data/app/assets/tailwind/lostcoords/ui/typography.css +31 -61
- data/app/assets/tailwind/lostcoords/ui/ui.css +2 -21
- data/app/assets/tailwind/lostcoords/ui/utilities.css +14 -22
- data/app/helpers/lostcoords/ui/badge_helper.rb +67 -110
- data/app/helpers/lostcoords/ui/button_helper.rb +44 -35
- data/app/helpers/lostcoords/ui/card_helper.rb +9 -52
- data/app/helpers/lostcoords/ui/choice_helper.rb +87 -0
- data/app/helpers/lostcoords/ui/code_block_helper.rb +22 -0
- data/app/helpers/lostcoords/ui/collapsible_helper.rb +9 -0
- data/app/helpers/lostcoords/ui/copy_button_helper.rb +7 -7
- data/app/helpers/lostcoords/ui/description_list_helper.rb +12 -0
- data/app/helpers/lostcoords/ui/dialog_helper.rb +15 -17
- data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +6 -0
- data/app/helpers/lostcoords/ui/feedback_helper.rb +25 -6
- data/app/helpers/lostcoords/ui/form_helper.rb +36 -59
- data/app/helpers/lostcoords/ui/glass_helper.rb +7 -7
- data/app/helpers/lostcoords/ui/layout_helper.rb +13 -11
- data/app/helpers/lostcoords/ui/menu_helper.rb +45 -10
- data/app/helpers/lostcoords/ui/navigation_helper.rb +144 -0
- data/app/helpers/lostcoords/ui/primitives_helper.rb +39 -61
- data/app/helpers/lostcoords/ui/reading_helper.rb +2 -2
- data/app/helpers/lostcoords/ui/render_attributes_helper.rb +17 -1
- data/app/helpers/lostcoords/ui/sheet_helper.rb +7 -45
- data/app/helpers/lostcoords/ui/structures_helper.rb +26 -125
- data/app/helpers/lostcoords/ui/table_helper.rb +11 -0
- data/app/helpers/lostcoords/ui/tailwind_helper.rb +13 -4
- data/app/helpers/lostcoords/ui/toast_helper.rb +22 -0
- data/app/helpers/lostcoords/ui/tooltip_helper.rb +17 -0
- data/app/helpers/lostcoords/ui/typography_helper.rb +41 -11
- data/app/javascript/lostcoords/ui/controllers/lc_code_block_controller.js +23 -0
- data/app/javascript/lostcoords/ui/controllers/{disclosure_controller.js → lc_collapsible_controller.js} +4 -4
- data/app/javascript/lostcoords/ui/controllers/{confirm_dialog_controller.js → lc_confirm_dialog_controller.js} +4 -4
- data/app/javascript/lostcoords/ui/controllers/{modal_controller.js → lc_dialog_controller.js} +1 -1
- data/app/javascript/lostcoords/ui/controllers/{emoji_picker_controller.js → lc_emoji_picker_controller.js} +38 -10
- data/app/javascript/lostcoords/ui/controllers/lc_kbd_controller.js +12 -0
- data/app/javascript/lostcoords/ui/controllers/{row_menu_controller.js → lc_menu_controller.js} +25 -3
- data/app/javascript/lostcoords/ui/controllers/{ui_popover_controller.js → lc_popover_controller.js} +1 -1
- data/app/javascript/lostcoords/ui/controllers/{ui_selection_controller.js → lc_selection_controller.js} +7 -15
- data/app/javascript/lostcoords/ui/controllers/{sheet_controller.js → lc_sheet_controller.js} +17 -17
- data/app/javascript/lostcoords/ui/controllers/lc_shortcut_controller.js +45 -0
- data/app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js +42 -0
- data/app/javascript/lostcoords/ui/controllers/{toast_controller.js → lc_toast_controller.js} +2 -2
- data/app/javascript/lostcoords/ui/controllers/lc_toast_region_controller.js +62 -0
- data/app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js +54 -0
- data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +77 -25
- data/app/javascript/lostcoords/ui/helpers/context_dialog.js +6 -6
- data/app/javascript/lostcoords/ui/helpers/dialog.js +2 -2
- data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +30 -7
- data/app/javascript/lostcoords/ui/helpers/floating_panel.js +7 -8
- data/app/javascript/lostcoords/ui/helpers/menu_keys.js +19 -18
- data/app/javascript/lostcoords/ui/helpers/roving_focus.js +175 -0
- data/app/javascript/lostcoords/ui/helpers/sheet_motion.js +4 -4
- data/app/javascript/lostcoords/ui/helpers/shortcuts.js +162 -0
- data/app/javascript/lostcoords/ui/helpers/top_layer.js +48 -10
- data/app/javascript/lostcoords/ui/index.js +57 -0
- data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +46 -46
- data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +3 -3
- data/app/views/lostcoords/ui/components/_lc_code_block.html.erb +18 -0
- data/app/views/lostcoords/ui/components/_lc_collapsible.html.erb +5 -0
- data/app/views/lostcoords/ui/components/_lc_dialog.html.erb +11 -8
- data/app/views/lostcoords/ui/components/_lc_floating_panel.html.erb +8 -0
- data/config/importmap.rb +10 -2
- data/config/locales/en.yml +23 -0
- data/config/locales/es.yml +23 -0
- data/config/locales/ja.yml +23 -0
- data/lib/lostcoords/ui/action_view.rb +6 -3
- data/lib/lostcoords/ui/builders.rb +2 -2
- data/lib/lostcoords/ui/form_builder.rb +126 -0
- data/lib/lostcoords/ui/parts.rb +154 -0
- data/lib/lostcoords/ui/stylesheets.rb +25 -0
- data/lib/lostcoords/ui/tokens.rb +24 -1
- data/lib/lostcoords/ui/version.rb +1 -1
- data/lib/tasks/lostcoords_ui.rake +11 -0
- metadata +54 -21
- data/app/assets/tailwind/lostcoords/ui/reading.css +0 -60
- data/app/views/lostcoords/ui/components/_form_alert.html.erb +0 -4
- data/app/views/lostcoords/ui/components/_lc_disclosure.html.erb +0 -5
- data/app/views/lostcoords/ui/components/_skeleton.html.erb +0 -4
- data/app/views/lostcoords/ui/components/_spinner.html.erb +0 -10
- data/app/views/lostcoords/ui/components/_ui_floating_panel.html.erb +0 -8
- data/config/helper_renames.json +0 -113
- data/script/migrate_helpers.rb +0 -136
- /data/app/javascript/lostcoords/ui/controllers/{ui_copy_controller.js → lc_copy_controller.js} +0 -0
- /data/app/javascript/lostcoords/ui/controllers/{ui_toggle_controller.js → lc_toggle_controller.js} +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["toggle"]
|
|
5
|
+
|
|
6
|
+
toggle() {
|
|
7
|
+
this.setExpanded(!this.element.hasAttribute("data-expanded"))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
copyFailed() {
|
|
11
|
+
this.setExpanded(true)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setExpanded(expanded) {
|
|
15
|
+
this.element.toggleAttribute("data-expanded", expanded)
|
|
16
|
+
if (!this.hasToggleTarget) return
|
|
17
|
+
|
|
18
|
+
this.toggleTarget.setAttribute("aria-expanded", String(expanded))
|
|
19
|
+
this.toggleTarget.textContent = expanded
|
|
20
|
+
? this.toggleTarget.dataset.collapseLabel
|
|
21
|
+
: this.toggleTarget.dataset.expandLabel
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import { prefersReducedMotion } from "helpers/viewport"
|
|
2
|
+
import { prefersReducedMotion } from "lostcoords/ui/helpers/viewport"
|
|
3
3
|
|
|
4
4
|
// Animates a <details> both ways and keeps what it reveals on screen. Only
|
|
5
5
|
// user toggles go through here: programmatic opens (validation) already scroll
|
|
6
|
-
// to the field they care about. Durations live in ui/
|
|
6
|
+
// to the field they care about. Durations live in ui/collapsible.css.
|
|
7
7
|
export default class extends Controller {
|
|
8
8
|
// Where the panel's height can't ease to `auto` in CSS (Safari), ease the
|
|
9
9
|
// <details> itself between measured heights.
|
|
@@ -40,7 +40,7 @@ export default class extends Controller {
|
|
|
40
40
|
from,
|
|
41
41
|
this.element.offsetHeight,
|
|
42
42
|
duration,
|
|
43
|
-
"--ease-out-expo",
|
|
43
|
+
"--lc-ease-out-expo",
|
|
44
44
|
)
|
|
45
45
|
animation.finished.then(
|
|
46
46
|
() => animation.cancel(),
|
|
@@ -55,7 +55,7 @@ export default class extends Controller {
|
|
|
55
55
|
this.element.toggleAttribute("data-closing", true)
|
|
56
56
|
const duration = Math.max(...this.transitionDurations())
|
|
57
57
|
if (!this.constructor.easesHeightInCss) {
|
|
58
|
-
this.animateHeight(this.element.offsetHeight, this.closedHeight(), duration, "--ease-out")
|
|
58
|
+
this.animateHeight(this.element.offsetHeight, this.closedHeight(), duration, "--lc-ease-out")
|
|
59
59
|
}
|
|
60
60
|
this.closeTimeout = setTimeout(() => {
|
|
61
61
|
this.element.open = false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import { showContextDialog } from "helpers/context_dialog"
|
|
3
|
-
import { registerDialog } from "helpers/dialog"
|
|
2
|
+
import { showContextDialog } from "lostcoords/ui/helpers/context_dialog"
|
|
3
|
+
import { registerDialog } from "lostcoords/ui/helpers/dialog"
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static targets = ["dialog"]
|
|
@@ -16,8 +16,8 @@ export default class extends Controller {
|
|
|
16
16
|
if (!dialog || dialog.open) return
|
|
17
17
|
|
|
18
18
|
this.activeDialog = dialog
|
|
19
|
-
if (dialog.matches(".
|
|
20
|
-
showContextDialog(dialog, this.element.querySelector("[data-
|
|
19
|
+
if (dialog.matches(".lc-dialog-context")) {
|
|
20
|
+
showContextDialog(dialog, this.element.querySelector("[data-lc-menu-target='popover']"))
|
|
21
21
|
} else {
|
|
22
22
|
dialog.showModal()
|
|
23
23
|
}
|
data/app/javascript/lostcoords/ui/controllers/{modal_controller.js → lc_dialog_controller.js}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import { registerDialog } from "helpers/dialog"
|
|
2
|
+
import { registerDialog } from "lostcoords/ui/helpers/dialog"
|
|
3
3
|
|
|
4
4
|
// Opens a dialog whose body is a Turbo frame fetched on open. It reloads on every
|
|
5
5
|
// open, so what it shows is never staler than the page that opened it.
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { CatalogPicker } from "helpers/catalog_picker"
|
|
2
|
-
import {
|
|
1
|
+
import { CatalogPicker } from "lostcoords/ui/helpers/catalog_picker"
|
|
2
|
+
import { registerDismissable } from "lostcoords/ui/helpers/dismiss_stack"
|
|
3
|
+
import { menuItems, navigateMenu } from "lostcoords/ui/helpers/menu_keys"
|
|
4
|
+
import { navigate } from "lostcoords/ui/helpers/roving_focus"
|
|
5
|
+
import { prefersReducedMotion } from "lostcoords/ui/helpers/viewport"
|
|
3
6
|
|
|
4
7
|
const TONE_MODIFIERS = ["", "\u{1F3FB}", "\u{1F3FC}", "\u{1F3FD}", "\u{1F3FE}", "\u{1F3FF}"]
|
|
5
|
-
const TONE_STORAGE_KEY = "lostcoords_ui:emoji-picker:tone"
|
|
8
|
+
const TONE_STORAGE_KEY = "lostcoords_ui:lc-emoji-picker:tone"
|
|
6
9
|
|
|
7
10
|
function markCurrent(element, current) {
|
|
8
11
|
element.setAttribute("aria-current", String(current))
|
|
@@ -25,7 +28,7 @@ export default class extends CatalogPicker {
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
static optionKey = "emoji"
|
|
28
|
-
static catalogClass = "emoji-picker__catalog"
|
|
31
|
+
static catalogClass = "lc-emoji-picker__catalog"
|
|
29
32
|
static previewTargets = ["previewEmoji", "previewName"]
|
|
30
33
|
|
|
31
34
|
connect() {
|
|
@@ -36,6 +39,7 @@ export default class extends CatalogPicker {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
disconnect() {
|
|
42
|
+
this.unregisterTones?.()
|
|
39
43
|
this.clearPopover()
|
|
40
44
|
super.disconnect()
|
|
41
45
|
}
|
|
@@ -151,13 +155,33 @@ export default class extends CatalogPicker {
|
|
|
151
155
|
})
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
navigateTabs(event) {
|
|
159
|
+
navigate(event, this.tabTargets, { orientation: "horizontal", wrap: true })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Escape closes the tone menu before the picker around it.
|
|
154
163
|
toggleTones(event) {
|
|
155
164
|
event.preventDefault()
|
|
156
165
|
event.stopPropagation()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.
|
|
160
|
-
|
|
166
|
+
if (!this.toneMenuTarget.hidden) return this.closeTones()
|
|
167
|
+
|
|
168
|
+
this.toneMenuTarget.hidden = false
|
|
169
|
+
this.toneToggleTarget.setAttribute("aria-expanded", "true")
|
|
170
|
+
this.syncTones()
|
|
171
|
+
this.unregisterTones = registerDismissable({
|
|
172
|
+
element: this.toneMenuTarget,
|
|
173
|
+
type: "popover",
|
|
174
|
+
close: () => this.closeTones(),
|
|
175
|
+
restoreFocus: () => this.toneToggleTarget.focus({ preventScroll: true }),
|
|
176
|
+
})
|
|
177
|
+
if (event.detail === 0) {
|
|
178
|
+
const items = menuItems(this.toneMenuTarget)
|
|
179
|
+
;(items.find((item) => item.getAttribute("aria-current") === "true") || items[0])?.focus()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
navigateTones(event) {
|
|
184
|
+
navigateMenu(event, this.toneMenuTarget, { orientation: "both" })
|
|
161
185
|
}
|
|
162
186
|
|
|
163
187
|
selectTone(event) {
|
|
@@ -174,7 +198,11 @@ export default class extends CatalogPicker {
|
|
|
174
198
|
|
|
175
199
|
closeTones() {
|
|
176
200
|
if (!this.hasToneMenuTarget) return
|
|
201
|
+
this.unregisterTones?.()
|
|
202
|
+
this.unregisterTones = null
|
|
203
|
+
const hadFocus = this.toneMenuTarget.contains(document.activeElement)
|
|
177
204
|
this.toneMenuTarget.hidden = true
|
|
205
|
+
if (hadFocus) this.toneToggleTarget.focus({ preventScroll: true })
|
|
178
206
|
this.toneToggleTarget.setAttribute("aria-expanded", "false")
|
|
179
207
|
}
|
|
180
208
|
|
|
@@ -255,7 +283,7 @@ export default class extends CatalogPicker {
|
|
|
255
283
|
renderRecent(emojis) {
|
|
256
284
|
if (!this.hasRecentTarget) return
|
|
257
285
|
|
|
258
|
-
const grid = this.recentTarget.querySelector(".emoji-picker__grid")
|
|
286
|
+
const grid = this.recentTarget.querySelector(".lc-emoji-picker__grid")
|
|
259
287
|
const count = this.recentTarget.querySelector(".count")
|
|
260
288
|
if (!grid) return
|
|
261
289
|
|
|
@@ -322,7 +350,7 @@ export default class extends CatalogPicker {
|
|
|
322
350
|
return {
|
|
323
351
|
duration,
|
|
324
352
|
settle: duration && 280,
|
|
325
|
-
transition: "transform 260ms var(--ease-out-expo, ease-out)",
|
|
353
|
+
transition: "transform 260ms var(--lc-ease-out-expo, ease-out)",
|
|
326
354
|
offscreen: "translateY(100%)",
|
|
327
355
|
}
|
|
328
356
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { formatShortcut } from "lostcoords/ui/helpers/shortcuts"
|
|
3
|
+
|
|
4
|
+
// Shows a key hint in the platform's own notation: "mod+k" reads ⌘ K on
|
|
5
|
+
// Apple platforms. The server renders the Ctrl form, which stays without JS.
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static values = { keys: String }
|
|
8
|
+
|
|
9
|
+
keysValueChanged() {
|
|
10
|
+
if (this.keysValue) this.element.textContent = formatShortcut(this.keysValue)
|
|
11
|
+
}
|
|
12
|
+
}
|
data/app/javascript/lostcoords/ui/controllers/{row_menu_controller.js → lc_menu_controller.js}
RENAMED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { FloatingPanel } from "helpers/floating_panel"
|
|
2
|
-
import {
|
|
1
|
+
import { FloatingPanel } from "lostcoords/ui/helpers/floating_panel"
|
|
2
|
+
import {
|
|
3
|
+
focusFirstMenuItem,
|
|
4
|
+
focusLastMenuItem,
|
|
5
|
+
navigateMenu,
|
|
6
|
+
} from "lostcoords/ui/helpers/menu_keys"
|
|
3
7
|
|
|
4
8
|
// The phone dock's action stack lays itself out over the fixed nav, so it
|
|
5
9
|
// passes lift: false and stays in place instead of lifting into the top layer.
|
|
@@ -9,15 +13,33 @@ export default class extends FloatingPanel {
|
|
|
9
13
|
this.focusOutHandler = this.handleFocusOut.bind(this)
|
|
10
14
|
this.pointerDownHandler = this.handlePointerDown.bind(this)
|
|
11
15
|
this.keydownHandler = (event) => navigateMenu(event, this.popoverTarget)
|
|
16
|
+
this.triggerKeydownHandler = this.handleTriggerKeydown.bind(this)
|
|
12
17
|
this.focusOutSuppressed = false
|
|
13
18
|
this.focusOutSuppressionTimer = null
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
connect() {
|
|
22
|
+
super.connect()
|
|
23
|
+
this.triggerTarget.addEventListener("keydown", this.triggerKeydownHandler)
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
disconnect() {
|
|
27
|
+
this.triggerTarget.removeEventListener("keydown", this.triggerKeydownHandler)
|
|
17
28
|
this.clearFocusOutSuppression()
|
|
18
29
|
super.disconnect()
|
|
19
30
|
}
|
|
20
31
|
|
|
32
|
+
// Arrow Down opens the menu on its first item, Arrow Up on its last.
|
|
33
|
+
handleTriggerKeydown(event) {
|
|
34
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return
|
|
35
|
+
if (event.altKey || event.ctrlKey || event.metaKey) return
|
|
36
|
+
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
this.open()
|
|
39
|
+
if (event.key === "ArrowDown") focusFirstMenuItem(this.popoverTarget)
|
|
40
|
+
else focusLastMenuItem(this.popoverTarget)
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
toggle(event) {
|
|
22
44
|
event.preventDefault()
|
|
23
45
|
event.stopPropagation()
|
|
@@ -29,7 +51,7 @@ export default class extends FloatingPanel {
|
|
|
29
51
|
|
|
30
52
|
toggleFromRow(event) {
|
|
31
53
|
if (this.popoverTarget.contains(event.target)) return
|
|
32
|
-
if (event.target.closest("a, button, input, select, textarea, [data-
|
|
54
|
+
if (event.target.closest("a, button, input, select, textarea, [data-lc-menu-ignore]")) return
|
|
33
55
|
|
|
34
56
|
event.preventDefault()
|
|
35
57
|
this.isOpen ? this.close() : this.open()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { navigate } from "lostcoords/ui/helpers/roving_focus"
|
|
2
3
|
|
|
3
4
|
export default class extends Controller {
|
|
4
5
|
static targets = ["option", "input"]
|
|
@@ -22,27 +23,18 @@ export default class extends Controller {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
select(event) {
|
|
25
|
-
const option = event.target.closest('[data-
|
|
26
|
+
const option = event.target.closest('[data-lc-selection-target~="option"]')
|
|
26
27
|
if (!this.options.includes(option)) return
|
|
27
28
|
this.apply(option)
|
|
28
29
|
this.changed(option)
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// All four arrows move and select, wrapping at the ends; Left and Right
|
|
33
|
+
// follow the text direction.
|
|
31
34
|
navigate(event) {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
ArrowRight: (current + 1) % this.options.length,
|
|
36
|
-
ArrowDown: (current + 1) % this.options.length,
|
|
37
|
-
ArrowLeft: (current - 1 + this.options.length) % this.options.length,
|
|
38
|
-
ArrowUp: (current - 1 + this.options.length) % this.options.length,
|
|
39
|
-
Home: 0,
|
|
40
|
-
End: this.options.length - 1,
|
|
41
|
-
}[event.key]
|
|
42
|
-
if (next === undefined) return
|
|
43
|
-
event.preventDefault()
|
|
44
|
-
const option = this.options[next]
|
|
45
|
-
option.focus()
|
|
35
|
+
const option = navigate(event, this.optionTargets, { orientation: "both", wrap: true })
|
|
36
|
+
if (!option) return
|
|
37
|
+
|
|
46
38
|
this.apply(option)
|
|
47
39
|
this.changed(option)
|
|
48
40
|
}
|
data/app/javascript/lostcoords/ui/controllers/{sheet_controller.js → lc_sheet_controller.js}
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import { holdDocumentScroll } from "helpers/dismiss_stack"
|
|
3
|
-
import { SHEET_TIMING, sheetDuration } from "helpers/sheet_motion"
|
|
4
|
-
import { isSheetViewport, prefersReducedMotion, TEXT_ENTRY } from "helpers/viewport"
|
|
2
|
+
import { holdDocumentScroll } from "lostcoords/ui/helpers/dismiss_stack"
|
|
3
|
+
import { SHEET_TIMING, sheetDuration } from "lostcoords/ui/helpers/sheet_motion"
|
|
4
|
+
import { isSheetViewport, prefersReducedMotion, TEXT_ENTRY } from "lostcoords/ui/helpers/viewport"
|
|
5
5
|
|
|
6
6
|
const PROJECTION_MS = 180
|
|
7
7
|
const MEDIUM_FRACTION = 0.5
|
|
8
8
|
const PAGE_SCALE_LOSS = 0.06
|
|
9
|
-
const NO_DRAG = "input[type=range], [data-sheet-no-drag]"
|
|
10
|
-
const HANDLE = "[data-sheet-target~='handle']"
|
|
9
|
+
const NO_DRAG = "input[type=range], [data-lc-sheet-no-drag]"
|
|
10
|
+
const HANDLE = "[data-lc-sheet-target~='handle']"
|
|
11
11
|
const GRABBER_ZONE = 28
|
|
12
12
|
const KEYBOARD_FREE = 0.5
|
|
13
13
|
|
|
@@ -71,9 +71,9 @@ export default class extends Controller {
|
|
|
71
71
|
// The keyboard rises over the sheet. Safari and in-app browsers shrink the
|
|
72
72
|
// page under it, which would drag a bottom-pinned sheet up and squash it, so
|
|
73
73
|
// the sheet is placed from the top at the height the screen had when it
|
|
74
|
-
// opened (see --sheet-floor in the CSS).
|
|
74
|
+
// opened (see --lc-sheet-floor in the CSS).
|
|
75
75
|
pinFloor() {
|
|
76
|
-
if (this.isSheet) this.element.style.setProperty("--sheet-floor", `${window.innerHeight}px`)
|
|
76
|
+
if (this.isSheet) this.element.style.setProperty("--lc-sheet-floor", `${window.innerHeight}px`)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
closed() {
|
|
@@ -330,7 +330,7 @@ export default class extends Controller {
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
setLargeness(p) {
|
|
333
|
-
this.element.style.setProperty("--sheet-p", p.toFixed(3))
|
|
333
|
+
this.element.style.setProperty("--lc-sheet-p", p.toFixed(3))
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
setPage(p) {
|
|
@@ -339,12 +339,12 @@ export default class extends Controller {
|
|
|
339
339
|
if (p <= 0) return
|
|
340
340
|
this.releaseScroll = holdDocumentScroll()
|
|
341
341
|
const lockedTop = -parseFloat(document.body.style.top || "0")
|
|
342
|
-
root.style.setProperty("--sheet-page-origin", `${window.scrollY + lockedTop}px`)
|
|
343
|
-
root.dataset.
|
|
342
|
+
root.style.setProperty("--lc-sheet-page-origin", `${window.scrollY + lockedTop}px`)
|
|
343
|
+
root.dataset.lcSheetPage = ""
|
|
344
344
|
this.ownsPage = true
|
|
345
345
|
}
|
|
346
|
-
root.style.setProperty("--sheet-page-p", p.toFixed(3))
|
|
347
|
-
root.style.setProperty("--sheet-page-scale", (1 - PAGE_SCALE_LOSS * p).toFixed(4))
|
|
346
|
+
root.style.setProperty("--lc-sheet-page-p", p.toFixed(3))
|
|
347
|
+
root.style.setProperty("--lc-sheet-page-scale", (1 - PAGE_SCALE_LOSS * p).toFixed(4))
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
animatePage(p) {
|
|
@@ -361,15 +361,15 @@ export default class extends Controller {
|
|
|
361
361
|
if (!this.ownsPage) return
|
|
362
362
|
this.ownsPage = false
|
|
363
363
|
const root = document.documentElement
|
|
364
|
-
delete root.dataset.
|
|
364
|
+
delete root.dataset.lcSheetPage
|
|
365
365
|
for (const name of ["p", "scale", "origin", "transition"])
|
|
366
|
-
root.style.removeProperty(`--sheet-page-${name}`)
|
|
366
|
+
root.style.removeProperty(`--lc-sheet-page-${name}`)
|
|
367
367
|
this.releaseScroll()
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
pageTransition(value = prefersReducedMotion() ? "0s" : SHEET_TIMING) {
|
|
371
371
|
if (this.ownsPage || value !== "0s")
|
|
372
|
-
document.documentElement.style.setProperty("--sheet-page-transition", value)
|
|
372
|
+
document.documentElement.style.setProperty("--lc-sheet-page-transition", value)
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
transition() {
|
|
@@ -396,8 +396,8 @@ export default class extends Controller {
|
|
|
396
396
|
style.transform = ""
|
|
397
397
|
style.willChange = ""
|
|
398
398
|
style.blockSize = ""
|
|
399
|
-
style.removeProperty("--sheet-p")
|
|
400
|
-
style.removeProperty("--sheet-floor")
|
|
399
|
+
style.removeProperty("--lc-sheet-p")
|
|
400
|
+
style.removeProperty("--lc-sheet-floor")
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
scrollerFor(target) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { isEditable } from "lostcoords/ui/helpers/roving_focus"
|
|
3
|
+
import { ariaKeyShortcuts, registerShortcut } from "lostcoords/ui/helpers/shortcuts"
|
|
4
|
+
|
|
5
|
+
// Binds page-wide keys to the element: a field takes focus and selects its
|
|
6
|
+
// text, anything else is clicked. The element announces its keys through
|
|
7
|
+
// aria-keyshortcuts. See helpers/shortcuts.js for the key syntax.
|
|
8
|
+
export default class extends Controller {
|
|
9
|
+
static values = { keys: String }
|
|
10
|
+
|
|
11
|
+
// Stimulus keeps values across a reconnect (a moved or permanent element)
|
|
12
|
+
// without calling keysValueChanged again, so connect registers too.
|
|
13
|
+
connect() {
|
|
14
|
+
this.register()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
keysValueChanged() {
|
|
18
|
+
if (this.connected) this.register()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
register() {
|
|
22
|
+
this.connected = true
|
|
23
|
+
this.unregister?.()
|
|
24
|
+
this.unregister = null
|
|
25
|
+
if (!this.keysValue) return
|
|
26
|
+
|
|
27
|
+
this.element.setAttribute("aria-keyshortcuts", ariaKeyShortcuts(this.keysValue))
|
|
28
|
+
this.unregister = registerShortcut(this.element, this.keysValue, () => this.run())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
disconnect() {
|
|
32
|
+
this.connected = false
|
|
33
|
+
this.unregister?.()
|
|
34
|
+
this.unregister = null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
run() {
|
|
38
|
+
if (isEditable(this.element)) {
|
|
39
|
+
this.element.focus()
|
|
40
|
+
this.element.select?.()
|
|
41
|
+
} else {
|
|
42
|
+
this.element.click()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { navigate } from "lostcoords/ui/helpers/roving_focus"
|
|
3
|
+
|
|
4
|
+
// The ARIA tabs pattern. Arrow keys follow the tab list's aria-orientation, and
|
|
5
|
+
// Home and End jump to the ends. With automatic activation, moving focus selects
|
|
6
|
+
// the tab; with manual activation, Enter or Space does.
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static targets = ["tab", "panel"]
|
|
9
|
+
static values = { activation: { type: String, default: "automatic" } }
|
|
10
|
+
|
|
11
|
+
select(event) {
|
|
12
|
+
this.show(event.currentTarget)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
navigate(event) {
|
|
16
|
+
const list = event.currentTarget.closest('[role="tablist"]')
|
|
17
|
+
const tab = navigate(event, this.tabTargets, {
|
|
18
|
+
orientation:
|
|
19
|
+
list?.getAttribute("aria-orientation") === "vertical" ? "vertical" : "horizontal",
|
|
20
|
+
wrap: true,
|
|
21
|
+
})
|
|
22
|
+
if (!tab) return
|
|
23
|
+
|
|
24
|
+
if (this.activationValue === "manual") this.focusOnly(tab)
|
|
25
|
+
else this.show(tab)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
focusOnly(tab) {
|
|
29
|
+
for (const candidate of this.tabTargets) candidate.tabIndex = candidate === tab ? 0 : -1
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
show(tab) {
|
|
33
|
+
for (const candidate of this.tabTargets) {
|
|
34
|
+
const selected = candidate === tab
|
|
35
|
+
candidate.setAttribute("aria-selected", String(selected))
|
|
36
|
+
candidate.tabIndex = selected ? 0 : -1
|
|
37
|
+
}
|
|
38
|
+
const panelId = tab.getAttribute("aria-controls")
|
|
39
|
+
for (const panel of this.panelTargets) panel.hidden = panel.id !== panelId
|
|
40
|
+
this.dispatch("change", { detail: { value: tab.dataset.value } })
|
|
41
|
+
}
|
|
42
|
+
}
|
data/app/javascript/lostcoords/ui/controllers/{toast_controller.js → lc_toast_controller.js}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import { prefersReducedMotion } from "helpers/viewport"
|
|
2
|
+
import { prefersReducedMotion } from "lostcoords/ui/helpers/viewport"
|
|
3
3
|
|
|
4
4
|
export default class extends Controller {
|
|
5
5
|
static values = { timeout: { type: Number, default: 4000 } }
|
|
@@ -7,7 +7,7 @@ export default class extends Controller {
|
|
|
7
7
|
connect() {
|
|
8
8
|
for (const sibling of this.element.parentElement?.children || []) {
|
|
9
9
|
if (sibling === this.element) continue
|
|
10
|
-
this.application.getControllerForElementAndIdentifier(sibling, "toast")?.dismiss()
|
|
10
|
+
this.application.getControllerForElementAndIdentifier(sibling, "lc-toast")?.dismiss()
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
this.startedAt = performance.now()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { topModalElement } from "lostcoords/ui/helpers/dismiss_stack"
|
|
3
|
+
import { hideTopLayer, showTopLayer, supportsTopLayer } from "lostcoords/ui/helpers/top_layer"
|
|
4
|
+
|
|
5
|
+
// Shows the region in the top layer while it holds toasts, so no sticky header,
|
|
6
|
+
// sheet or dialog paints over them. Each new toast re-opens the region, which
|
|
7
|
+
// moves it above anything opened since. A modal dialog makes the rest of the
|
|
8
|
+
// page inert, so while one is open the region moves inside it, keeping toasts
|
|
9
|
+
// clickable and announced, and moves back when it closes. Without the Popover
|
|
10
|
+
// API the region stays fixed at --lc-z-toast.
|
|
11
|
+
const homes = new WeakMap()
|
|
12
|
+
|
|
13
|
+
export default class extends Controller {
|
|
14
|
+
connect() {
|
|
15
|
+
if (!supportsTopLayer()) return
|
|
16
|
+
|
|
17
|
+
this.observer = new MutationObserver((records) => {
|
|
18
|
+
if (records.some((record) => record.addedNodes.length > 0)) this.raise()
|
|
19
|
+
else if (this.element.childElementCount === 0) this.lower()
|
|
20
|
+
})
|
|
21
|
+
this.observer.observe(this.element, { childList: true })
|
|
22
|
+
if (this.element.childElementCount > 0) this.raise()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A modal removed right after closing (before its close event) takes the
|
|
26
|
+
// region with it; put the region back in the page.
|
|
27
|
+
disconnect() {
|
|
28
|
+
this.observer?.disconnect()
|
|
29
|
+
hideTopLayer(this.element)
|
|
30
|
+
if (!this.element.isConnected) this.goHome()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
raise() {
|
|
34
|
+
const modal = topModalElement()
|
|
35
|
+
if (modal && !modal.contains(this.element)) return this.moveInto(modal)
|
|
36
|
+
|
|
37
|
+
hideTopLayer(this.element)
|
|
38
|
+
showTopLayer(this.element)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
lower() {
|
|
42
|
+
hideTopLayer(this.element)
|
|
43
|
+
this.goHome()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Moving the region reconnects this controller, which raises it again.
|
|
47
|
+
moveInto(modal) {
|
|
48
|
+
if (!homes.has(this.element))
|
|
49
|
+
homes.set(this.element, { parent: this.element.parentNode, next: this.element.nextSibling })
|
|
50
|
+
const region = this.element
|
|
51
|
+
modal.addEventListener("close", () => this.goHome(region), { once: true })
|
|
52
|
+
modal.append(region)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
goHome(region = this.element) {
|
|
56
|
+
const home = homes.get(region)
|
|
57
|
+
if (!home?.parent.isConnected) return
|
|
58
|
+
|
|
59
|
+
homes.delete(region)
|
|
60
|
+
home.parent.insertBefore(region, home.next?.parentNode === home.parent ? home.next : null)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { registerDismissable } from "lostcoords/ui/helpers/dismiss_stack"
|
|
3
|
+
import { liftToTopLayer, onPageLeave } from "lostcoords/ui/helpers/top_layer"
|
|
4
|
+
|
|
5
|
+
// Opens the tooltip in the top layer, so no stacking context or overflow clip
|
|
6
|
+
// covers it. Without the Popover API it stays in place. Escape closes it before
|
|
7
|
+
// any menu or dialog around it, and it stays hidden until the pointer leaves
|
|
8
|
+
// and focus moves away.
|
|
9
|
+
export default class extends Controller {
|
|
10
|
+
static targets = ["content"]
|
|
11
|
+
|
|
12
|
+
connect() {
|
|
13
|
+
this.stopPageLeave = onPageLeave(() => this.close({ immediate: true }))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
disconnect() {
|
|
17
|
+
this.stopPageLeave?.()
|
|
18
|
+
this.close({ immediate: true })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
show(event) {
|
|
22
|
+
if (event.type === "focusin" && !event.target.matches(":focus-visible")) return
|
|
23
|
+
if (this.release || this.element.hasAttribute("data-dismissed")) return
|
|
24
|
+
|
|
25
|
+
this.element.toggleAttribute("data-open", true)
|
|
26
|
+
this.release = liftToTopLayer(this.contentTarget, this.element)
|
|
27
|
+
this.unregister = registerDismissable({
|
|
28
|
+
type: "tooltip",
|
|
29
|
+
element: this.element,
|
|
30
|
+
isOpen: () => this.element.hasAttribute("data-open"),
|
|
31
|
+
close: () => this.dismiss(),
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
hide() {
|
|
36
|
+
if (this.element.matches(":hover") || this.element.querySelector(":focus-visible")) return
|
|
37
|
+
|
|
38
|
+
delete this.element.dataset.dismissed
|
|
39
|
+
this.close()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
dismiss() {
|
|
43
|
+
this.element.dataset.dismissed = ""
|
|
44
|
+
this.close({ immediate: true })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
close({ immediate = false } = {}) {
|
|
48
|
+
this.element.toggleAttribute("data-open", false)
|
|
49
|
+
this.release?.({ immediate })
|
|
50
|
+
this.release = null
|
|
51
|
+
this.unregister?.()
|
|
52
|
+
this.unregister = null
|
|
53
|
+
}
|
|
54
|
+
}
|