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,162 @@
|
|
|
1
|
+
import { topModalElement } from "lostcoords/ui/helpers/dismiss_stack"
|
|
2
|
+
import { isEditable } from "lostcoords/ui/helpers/roving_focus"
|
|
3
|
+
|
|
4
|
+
// Page-wide keyboard shortcuts. A shortcut is written as keys joined by "+",
|
|
5
|
+
// with "mod" for ⌘ on Apple platforms and Ctrl elsewhere: "mod+k", "/",
|
|
6
|
+
// "shift+?", "alt+n". Several shortcuts for one element are separated by spaces.
|
|
7
|
+
//
|
|
8
|
+
// A shortcut without mod, ctrl or alt never fires while focus is in a text
|
|
9
|
+
// field, so typing "/" in a field types it. While a modal dialog is open, only
|
|
10
|
+
// elements inside it respond. When two visible elements claim the same keys,
|
|
11
|
+
// the first in document order wins and the page logs a warning.
|
|
12
|
+
|
|
13
|
+
const bindings = new Set()
|
|
14
|
+
const warned = new Set()
|
|
15
|
+
|
|
16
|
+
export const isApple = /mac|iphone|ipad|ipod/i.test(
|
|
17
|
+
navigator.userAgentData?.platform || navigator.platform || navigator.userAgent,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
export function parseShortcuts(text) {
|
|
21
|
+
return text
|
|
22
|
+
.trim()
|
|
23
|
+
.split(/\s+/)
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((combo) => {
|
|
26
|
+
const parts = combo.toLowerCase().split("+")
|
|
27
|
+
const key = parts.pop() || "+"
|
|
28
|
+
const modifiers = new Set(parts)
|
|
29
|
+
return {
|
|
30
|
+
text: combo,
|
|
31
|
+
key,
|
|
32
|
+
mod: modifiers.has("mod"),
|
|
33
|
+
ctrl: modifiers.has("ctrl"),
|
|
34
|
+
meta: modifiers.has("meta"),
|
|
35
|
+
alt: modifiers.has("alt"),
|
|
36
|
+
shift: modifiers.has("shift"),
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function matchesShortcut(shortcut, event) {
|
|
42
|
+
const meta = shortcut.meta || (shortcut.mod && isApple)
|
|
43
|
+
const ctrl = shortcut.ctrl || (shortcut.mod && !isApple)
|
|
44
|
+
if (event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== shortcut.alt)
|
|
45
|
+
return false
|
|
46
|
+
|
|
47
|
+
// Option on a Mac turns letters into other characters (Option+N is a dead key),
|
|
48
|
+
// so alt shortcuts on letters and digits match the physical key.
|
|
49
|
+
if (shortcut.alt && /^[a-z0-9]$/.test(shortcut.key))
|
|
50
|
+
return event.shiftKey === shortcut.shift && event.code === physicalKey(shortcut.key)
|
|
51
|
+
|
|
52
|
+
const key = event.key.toLowerCase()
|
|
53
|
+
// Shifted punctuation such as "?" already reports the shifted key.
|
|
54
|
+
if (key.length === 1 && !/[a-z0-9]/.test(key)) return key === shortcut.key
|
|
55
|
+
return event.shiftKey === shortcut.shift && key === shortcut.key
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const physicalKey = (key) => (/\d/.test(key) ? `Digit${key}` : `Key${key.toUpperCase()}`)
|
|
59
|
+
|
|
60
|
+
// Shift shows unless the key is a symbol it already produces, such as "?".
|
|
61
|
+
const showsShift = (shortcut) => shortcut.shift && !/^[^a-z0-9]$/.test(shortcut.key)
|
|
62
|
+
|
|
63
|
+
const KEY_NAMES = {
|
|
64
|
+
escape: "Esc",
|
|
65
|
+
arrowup: "↑",
|
|
66
|
+
arrowdown: "↓",
|
|
67
|
+
arrowleft: "←",
|
|
68
|
+
arrowright: "→",
|
|
69
|
+
enter: "↵",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// "mod+k" reads "⌘ K" on Apple platforms and "Ctrl K" elsewhere.
|
|
73
|
+
export function formatShortcut(text) {
|
|
74
|
+
return parseShortcuts(text)
|
|
75
|
+
.map((shortcut) => {
|
|
76
|
+
const keys = []
|
|
77
|
+
if (shortcut.ctrl || (shortcut.mod && !isApple)) keys.push(isApple ? "⌃" : "Ctrl")
|
|
78
|
+
if (shortcut.alt) keys.push(isApple ? "⌥" : "Alt")
|
|
79
|
+
if (showsShift(shortcut)) keys.push(isApple ? "⇧" : "Shift")
|
|
80
|
+
if (shortcut.meta || (shortcut.mod && isApple)) keys.push(isApple ? "⌘" : "Meta")
|
|
81
|
+
keys.push(
|
|
82
|
+
KEY_NAMES[shortcut.key] ||
|
|
83
|
+
(shortcut.key.length === 1 ? shortcut.key.toUpperCase() : shortcut.key),
|
|
84
|
+
)
|
|
85
|
+
return keys.join(" ")
|
|
86
|
+
})
|
|
87
|
+
.join(" / ")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The aria-keyshortcuts value: "mod+k" becomes "Meta+K" or "Control+K".
|
|
91
|
+
export function ariaKeyShortcuts(text) {
|
|
92
|
+
return parseShortcuts(text)
|
|
93
|
+
.map((shortcut) => {
|
|
94
|
+
const keys = []
|
|
95
|
+
if (shortcut.ctrl || (shortcut.mod && !isApple)) keys.push("Control")
|
|
96
|
+
if (shortcut.alt) keys.push("Alt")
|
|
97
|
+
if (shortcut.shift) keys.push("Shift")
|
|
98
|
+
if (shortcut.meta || (shortcut.mod && isApple)) keys.push("Meta")
|
|
99
|
+
keys.push(
|
|
100
|
+
shortcut.key.length === 1
|
|
101
|
+
? shortcut.key.toUpperCase()
|
|
102
|
+
: shortcut.key[0].toUpperCase() + shortcut.key.slice(1),
|
|
103
|
+
)
|
|
104
|
+
return keys.join("+")
|
|
105
|
+
})
|
|
106
|
+
.join(" ")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Registers element for the shortcuts in text; run is called when one fires.
|
|
110
|
+
// Returns a function that removes the binding.
|
|
111
|
+
export function registerShortcut(element, text, run) {
|
|
112
|
+
const binding = { element, shortcuts: parseShortcuts(text), run }
|
|
113
|
+
if (bindings.size === 0) document.addEventListener("keydown", handleKeydown)
|
|
114
|
+
bindings.add(binding)
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
bindings.delete(binding)
|
|
118
|
+
if (bindings.size === 0) document.removeEventListener("keydown", handleKeydown)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function handleKeydown(event) {
|
|
123
|
+
if (event.defaultPrevented || event.isComposing || event.repeat) return
|
|
124
|
+
|
|
125
|
+
const typing = isEditable(event.target)
|
|
126
|
+
const matches = [...bindings].filter(
|
|
127
|
+
(binding) =>
|
|
128
|
+
binding.shortcuts.some(
|
|
129
|
+
(shortcut) =>
|
|
130
|
+
matchesShortcut(shortcut, event) &&
|
|
131
|
+
!(typing && !shortcut.mod && !shortcut.ctrl && !shortcut.meta && !shortcut.alt),
|
|
132
|
+
) && available(binding.element),
|
|
133
|
+
)
|
|
134
|
+
if (matches.length === 0) return
|
|
135
|
+
|
|
136
|
+
matches.sort((a, b) =>
|
|
137
|
+
a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1,
|
|
138
|
+
)
|
|
139
|
+
if (matches.length > 1) warnConflict(event, matches)
|
|
140
|
+
|
|
141
|
+
event.preventDefault()
|
|
142
|
+
matches[0].run(event)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function available(element) {
|
|
146
|
+
if (!element.isConnected || element.disabled || element.closest("[inert]")) return false
|
|
147
|
+
if (element.getClientRects().length === 0) return false
|
|
148
|
+
|
|
149
|
+
const modal = topModalElement()
|
|
150
|
+
return !modal || modal.contains(element)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function warnConflict(event, matches) {
|
|
154
|
+
const key = `${event.ctrlKey}${event.metaKey}${event.altKey}${event.shiftKey}${event.key}`
|
|
155
|
+
if (warned.has(key)) return
|
|
156
|
+
|
|
157
|
+
warned.add(key)
|
|
158
|
+
console.warn(
|
|
159
|
+
`lostcoords-ui: ${matches.length} elements share the shortcut ${event.key}; the first one runs.`,
|
|
160
|
+
matches.map((binding) => binding.element),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -6,17 +6,48 @@
|
|
|
6
6
|
// The panel's stylesheet still decides where it sits. The caller makes the
|
|
7
7
|
// panel visible in place, liftToTopLayer measures that designed spot relative
|
|
8
8
|
// to its trigger, then re-pins it there as a fixed top-layer popover, follows
|
|
9
|
-
// the trigger on scroll and resize, and flips
|
|
10
|
-
// viewport. Browsers without the Popover API keep the in-place panel.
|
|
9
|
+
// the trigger on scroll and resize, and flips to the other side when it would
|
|
10
|
+
// leave the viewport. Browsers without the Popover API keep the in-place panel.
|
|
11
|
+
//
|
|
12
|
+
// Every top-layer call in the library goes through this file: showTopLayer and
|
|
13
|
+
// hideTopLayer for elements that place themselves, liftToTopLayer for panels
|
|
14
|
+
// pinned to a trigger, and onPageLeave to close them before Turbo caches a page.
|
|
11
15
|
|
|
12
16
|
const MARGIN = 8
|
|
17
|
+
const PAGE_LEAVES = ["turbo:before-cache", "turbo:before-visit", "turbo:before-render"]
|
|
18
|
+
|
|
19
|
+
export const supportsTopLayer = () => typeof HTMLElement.prototype.showPopover === "function"
|
|
20
|
+
|
|
21
|
+
// showPopover and hidePopover throw when the element is disconnected or already
|
|
22
|
+
// in that state; both are no-ops here instead.
|
|
23
|
+
export function showTopLayer(element) {
|
|
24
|
+
if (!supportsTopLayer() || !element.isConnected) return false
|
|
25
|
+
if (!element.hasAttribute("popover")) element.setAttribute("popover", "manual")
|
|
26
|
+
if (!element.matches(":popover-open")) element.showPopover()
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hideTopLayer(element) {
|
|
31
|
+
if (supportsTopLayer() && element.matches(":popover-open")) element.hidePopover()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Calls close before Turbo snapshots or leaves the page, so a cached page never
|
|
35
|
+
// keeps an open overlay. Returns a function that removes the listeners.
|
|
36
|
+
export function onPageLeave(close) {
|
|
37
|
+
for (const type of PAGE_LEAVES) document.addEventListener(type, close)
|
|
38
|
+
window.addEventListener("pagehide", close)
|
|
39
|
+
return () => {
|
|
40
|
+
for (const type of PAGE_LEAVES) document.removeEventListener(type, close)
|
|
41
|
+
window.removeEventListener("pagehide", close)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
13
44
|
const PLACEMENT = ["position", "inset", "top", "left", "margin", "translate", "inlineSize"]
|
|
14
45
|
|
|
15
46
|
const lifts = new WeakMap()
|
|
16
47
|
const pinned = new WeakSet()
|
|
17
48
|
|
|
18
49
|
export function liftToTopLayer(panel, trigger) {
|
|
19
|
-
if (
|
|
50
|
+
if (!supportsTopLayer() || !panel.isConnected) return () => {}
|
|
20
51
|
|
|
21
52
|
lifts.get(panel)?.()
|
|
22
53
|
if (pinned.has(panel)) reset(panel)
|
|
@@ -25,10 +56,12 @@ export function liftToTopLayer(panel, trigger) {
|
|
|
25
56
|
const placed = panel.getBoundingClientRect()
|
|
26
57
|
const offsetX = placed.left - anchor.left
|
|
27
58
|
const offsetY = placed.top - anchor.top
|
|
28
|
-
|
|
59
|
+
// A panel above its trigger flips below when it would leave the top edge; any
|
|
60
|
+
// other panel flips above when it would leave the bottom.
|
|
61
|
+
const above = placed.bottom <= anchor.top + 1
|
|
62
|
+
const gap = above ? anchor.top - placed.bottom : placed.top - anchor.bottom
|
|
29
63
|
|
|
30
|
-
panel
|
|
31
|
-
panel.showPopover()
|
|
64
|
+
showTopLayer(panel)
|
|
32
65
|
Object.assign(panel.style, {
|
|
33
66
|
position: "fixed",
|
|
34
67
|
inset: "auto",
|
|
@@ -48,8 +81,13 @@ export function liftToTopLayer(panel, trigger) {
|
|
|
48
81
|
const right = document.documentElement.clientWidth - MARGIN
|
|
49
82
|
|
|
50
83
|
let top = a.top + offsetY
|
|
51
|
-
|
|
52
|
-
|
|
84
|
+
if (above) {
|
|
85
|
+
const flipped = a.bottom + gap
|
|
86
|
+
if (top < minTop + MARGIN && flipped + height <= maxBottom - MARGIN) top = flipped
|
|
87
|
+
} else {
|
|
88
|
+
const flipped = a.top - gap - height
|
|
89
|
+
if (top + height > maxBottom - MARGIN && flipped >= minTop + MARGIN) top = flipped
|
|
90
|
+
}
|
|
53
91
|
top = Math.max(minTop + MARGIN, Math.min(top, maxBottom - MARGIN - height))
|
|
54
92
|
|
|
55
93
|
panel.style.left = `${Math.max(MARGIN, Math.min(a.left + offsetX, right - width))}px`
|
|
@@ -77,7 +115,7 @@ export function liftToTopLayer(panel, trigger) {
|
|
|
77
115
|
Promise.allSettled(panel.getAnimations().map((animation) => animation.finished)).then(() => {
|
|
78
116
|
if (!lifts.has(panel)) reset(panel)
|
|
79
117
|
})
|
|
80
|
-
|
|
118
|
+
hideTopLayer(panel)
|
|
81
119
|
}
|
|
82
120
|
lifts.set(panel, release)
|
|
83
121
|
return release
|
|
@@ -85,7 +123,7 @@ export function liftToTopLayer(panel, trigger) {
|
|
|
85
123
|
|
|
86
124
|
function reset(panel) {
|
|
87
125
|
pinned.delete(panel)
|
|
88
|
-
|
|
126
|
+
hideTopLayer(panel)
|
|
89
127
|
panel.removeAttribute("popover")
|
|
90
128
|
for (const property of PLACEMENT) panel.style[property] = ""
|
|
91
129
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import CodeBlock from "lostcoords/ui/controllers/lc_code_block_controller"
|
|
2
|
+
import Collapsible from "lostcoords/ui/controllers/lc_collapsible_controller"
|
|
3
|
+
import ConfirmDialog from "lostcoords/ui/controllers/lc_confirm_dialog_controller"
|
|
4
|
+
import Copy from "lostcoords/ui/controllers/lc_copy_controller"
|
|
5
|
+
import Dialog from "lostcoords/ui/controllers/lc_dialog_controller"
|
|
6
|
+
import EmojiPicker from "lostcoords/ui/controllers/lc_emoji_picker_controller"
|
|
7
|
+
import Kbd from "lostcoords/ui/controllers/lc_kbd_controller"
|
|
8
|
+
import Menu from "lostcoords/ui/controllers/lc_menu_controller"
|
|
9
|
+
import Popover from "lostcoords/ui/controllers/lc_popover_controller"
|
|
10
|
+
import Selection from "lostcoords/ui/controllers/lc_selection_controller"
|
|
11
|
+
import Sheet from "lostcoords/ui/controllers/lc_sheet_controller"
|
|
12
|
+
import Shortcut from "lostcoords/ui/controllers/lc_shortcut_controller"
|
|
13
|
+
import Tabs from "lostcoords/ui/controllers/lc_tabs_controller"
|
|
14
|
+
import Toast from "lostcoords/ui/controllers/lc_toast_controller"
|
|
15
|
+
import ToastRegion from "lostcoords/ui/controllers/lc_toast_region_controller"
|
|
16
|
+
import Toggle from "lostcoords/ui/controllers/lc_toggle_controller"
|
|
17
|
+
import Tooltip from "lostcoords/ui/controllers/lc_tooltip_controller"
|
|
18
|
+
|
|
19
|
+
const controllers = {
|
|
20
|
+
"lc-confirm-dialog": ConfirmDialog,
|
|
21
|
+
"lc-collapsible": Collapsible,
|
|
22
|
+
"lc-code-block": CodeBlock,
|
|
23
|
+
"lc-copy": Copy,
|
|
24
|
+
"lc-dialog": Dialog,
|
|
25
|
+
"lc-emoji-picker": EmojiPicker,
|
|
26
|
+
"lc-kbd": Kbd,
|
|
27
|
+
"lc-menu": Menu,
|
|
28
|
+
"lc-popover": Popover,
|
|
29
|
+
"lc-selection": Selection,
|
|
30
|
+
"lc-sheet": Sheet,
|
|
31
|
+
"lc-shortcut": Shortcut,
|
|
32
|
+
"lc-tabs": Tabs,
|
|
33
|
+
"lc-toast": Toast,
|
|
34
|
+
"lc-toast-region": ToastRegion,
|
|
35
|
+
"lc-toggle": Toggle,
|
|
36
|
+
"lc-tooltip": Tooltip,
|
|
37
|
+
}
|
|
38
|
+
const registrations = new WeakMap()
|
|
39
|
+
|
|
40
|
+
export function registerControllers(application, { only = Object.keys(controllers) } = {}) {
|
|
41
|
+
if (!Array.isArray(only)) throw new TypeError("only must be an array of controller identifiers")
|
|
42
|
+
for (const identifier of only) {
|
|
43
|
+
if (!Object.hasOwn(controllers, identifier)) {
|
|
44
|
+
throw new Error(`Unknown lostcoords-ui controller: ${identifier}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
let registered = registrations.get(application)
|
|
48
|
+
if (!registered) {
|
|
49
|
+
registered = new Set()
|
|
50
|
+
registrations.set(application, registered)
|
|
51
|
+
}
|
|
52
|
+
for (const identifier of only) {
|
|
53
|
+
if (registered.has(identifier)) continue
|
|
54
|
+
application.register(identifier, controllers[identifier])
|
|
55
|
+
registered.add(identifier)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -7,30 +7,30 @@
|
|
|
7
7
|
tone_modifiers = [ "", "🏻", "🏼", "🏽", "🏾", "🏿" ]
|
|
8
8
|
tone_names = %w[default light medium_light medium medium_dark dark] %>
|
|
9
9
|
|
|
10
|
-
<%= tag.div(**lc_attributes({ class: lc_tw("emoji-picker", picker_class), data: { controller: "emoji-picker",
|
|
10
|
+
<%= tag.div(**lc_attributes({ class: lc_tw("lc-emoji-picker", picker_class), data: { controller: "lc-emoji-picker", lc_emoji_picker_auto_submit_value: auto_submit, lc_emoji_picker_catalog_url_value: catalog_url, lc_emoji_picker_recent_scope_value: recent_scope } }, attributes)) do %>
|
|
11
11
|
<%= form.hidden_field field,
|
|
12
12
|
value: selected,
|
|
13
|
-
data: {
|
|
13
|
+
data: { lc_emoji_picker_target: "input" } %>
|
|
14
14
|
|
|
15
15
|
<% quick.each do |emoji| %>
|
|
16
16
|
<button
|
|
17
17
|
type="button"
|
|
18
|
-
class="emoji-picker__choice"
|
|
18
|
+
class="lc-emoji-picker__choice"
|
|
19
19
|
aria-current="<%= emoji == selected %>"
|
|
20
20
|
data-emoji="<%= emoji %>"
|
|
21
|
-
data-action="click->emoji-picker#select"
|
|
21
|
+
data-action="click->lc-emoji-picker#select"
|
|
22
22
|
>
|
|
23
23
|
<%= emoji %>
|
|
24
24
|
</button>
|
|
25
25
|
<% end %>
|
|
26
26
|
|
|
27
|
-
<div class="emoji-picker__more">
|
|
27
|
+
<div class="lc-emoji-picker__more">
|
|
28
28
|
<button
|
|
29
29
|
type="button"
|
|
30
|
-
class="emoji-picker__trigger <%= trigger_class %>"
|
|
30
|
+
class="lc-emoji-picker__trigger <%= trigger_class %>"
|
|
31
31
|
aria-current="<%= !selected_is_quick %>"
|
|
32
|
-
data-emoji-picker-target="trigger"
|
|
33
|
-
data-action="click->emoji-picker#toggle"
|
|
32
|
+
data-lc-emoji-picker-target="trigger"
|
|
33
|
+
data-action="click->lc-emoji-picker#toggle"
|
|
34
34
|
data-default-label="+"
|
|
35
35
|
data-value="<%= selected %>"
|
|
36
36
|
aria-label="<%= label %>"
|
|
@@ -40,25 +40,25 @@
|
|
|
40
40
|
</button>
|
|
41
41
|
|
|
42
42
|
<div
|
|
43
|
-
class="emoji-picker__popover <%= popover_class %>"
|
|
44
|
-
data-emoji-picker-target="popover"
|
|
45
|
-
data-controller="sheet"
|
|
46
|
-
data-action="click->emoji-picker#select pointerover->emoji-picker#preview focusin->emoji-picker#preview sheet:dismiss->emoji-picker#close"
|
|
43
|
+
class="lc-emoji-picker__popover <%= popover_class %>"
|
|
44
|
+
data-lc-emoji-picker-target="popover"
|
|
45
|
+
data-controller="lc-sheet"
|
|
46
|
+
data-action="click->lc-emoji-picker#select pointerover->lc-emoji-picker#preview focusin->lc-emoji-picker#preview lc-sheet:dismiss->lc-emoji-picker#close"
|
|
47
47
|
role="dialog"
|
|
48
48
|
aria-label="<%= label %>"
|
|
49
49
|
hidden
|
|
50
50
|
>
|
|
51
|
-
<span class="emoji-picker__handle" data-sheet-target="handle" aria-hidden="true"></span>
|
|
51
|
+
<span class="lc-emoji-picker__handle" data-lc-sheet-target="handle" aria-hidden="true"></span>
|
|
52
52
|
|
|
53
|
-
<div class="emoji-picker__search">
|
|
54
|
-
<div class="emoji-picker__search-field">
|
|
55
|
-
<svg class="emoji-picker__search-icon" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
53
|
+
<div class="lc-emoji-picker__search">
|
|
54
|
+
<div class="lc-emoji-picker__search-field">
|
|
55
|
+
<svg class="lc-emoji-picker__search-icon" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
56
56
|
<circle cx="9" cy="9" r="6" stroke="currentColor" stroke-width="1.6" />
|
|
57
57
|
<path d="m14 14 3.5 3.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
|
|
58
58
|
</svg>
|
|
59
59
|
|
|
60
60
|
<input
|
|
61
|
-
class="emoji-picker__search-input"
|
|
61
|
+
class="lc-emoji-picker__search-input"
|
|
62
62
|
type="search"
|
|
63
63
|
inputmode="search"
|
|
64
64
|
autocomplete="off"
|
|
@@ -67,30 +67,30 @@
|
|
|
67
67
|
enterkeyhint="search"
|
|
68
68
|
aria-label="<%= lc_t("lostcoords.ui.emoji_picker.search_label") %>"
|
|
69
69
|
placeholder="<%= lc_t("lostcoords.ui.emoji_picker.search_placeholder") %>"
|
|
70
|
-
data-emoji-picker-target="searchInput"
|
|
71
|
-
data-action="input->emoji-picker#filter keydown->emoji-picker#onSearchKeydown"
|
|
70
|
+
data-lc-emoji-picker-target="searchInput"
|
|
71
|
+
data-action="input->lc-emoji-picker#filter keydown->lc-emoji-picker#onSearchKeydown"
|
|
72
72
|
>
|
|
73
73
|
</div>
|
|
74
74
|
|
|
75
|
-
<div class="emoji-picker__tones" data-emoji-picker-target="tones">
|
|
75
|
+
<div class="lc-emoji-picker__tones" data-lc-emoji-picker-target="tones">
|
|
76
76
|
<button
|
|
77
77
|
type="button"
|
|
78
|
-
class="emoji-picker__tone-toggle"
|
|
79
|
-
data-emoji-picker-target="toneToggle"
|
|
80
|
-
data-action="click->emoji-picker#toggleTones"
|
|
78
|
+
class="lc-emoji-picker__tone-toggle"
|
|
79
|
+
data-lc-emoji-picker-target="toneToggle"
|
|
80
|
+
data-action="click->lc-emoji-picker#toggleTones"
|
|
81
81
|
aria-haspopup="true"
|
|
82
82
|
aria-expanded="false"
|
|
83
83
|
aria-label="<%= lc_t("lostcoords.ui.emoji_picker.skin_tone") %>"
|
|
84
84
|
>✋</button>
|
|
85
85
|
|
|
86
|
-
<div class="emoji-picker__tone-menu <%= popover_class %>" data-emoji-picker-target="toneMenu" role="menu" hidden>
|
|
86
|
+
<div class="lc-emoji-picker__tone-menu <%= popover_class %>" data-lc-emoji-picker-target="toneMenu" data-action="keydown->lc-emoji-picker#navigateTones" role="menu" aria-label="<%= lc_t("lostcoords.ui.emoji_picker.skin_tone") %>" hidden>
|
|
87
87
|
<% tone_modifiers.each_with_index do |modifier, index| %>
|
|
88
88
|
<button
|
|
89
89
|
type="button"
|
|
90
|
-
class="emoji-picker__tone"
|
|
90
|
+
class="lc-emoji-picker__tone"
|
|
91
91
|
role="menuitemradio"
|
|
92
92
|
data-tone-index="<%= index %>"
|
|
93
|
-
data-action="click->emoji-picker#selectTone"
|
|
93
|
+
data-action="click->lc-emoji-picker#selectTone"
|
|
94
94
|
aria-label="<%= lc_t("lostcoords.ui.emoji_picker.skin_tones.#{tone_names[index]}") %>"
|
|
95
95
|
>✋<%= modifier %></button>
|
|
96
96
|
<% end %>
|
|
@@ -98,14 +98,14 @@
|
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
100
100
|
|
|
101
|
-
<nav class="emoji-picker__tabs" data-emoji-picker-target="tabs" aria-label="<%= lc_t("lostcoords.ui.emoji_picker.categories_nav") %>">
|
|
101
|
+
<nav class="lc-emoji-picker__tabs" data-lc-emoji-picker-target="tabs" data-action="keydown->lc-emoji-picker#navigateTabs" aria-label="<%= lc_t("lostcoords.ui.emoji_picker.categories_nav") %>">
|
|
102
102
|
<% if recent.any? %>
|
|
103
103
|
<button
|
|
104
104
|
type="button"
|
|
105
|
-
class="emoji-picker__tab"
|
|
106
|
-
data-emoji-picker-target="tab"
|
|
105
|
+
class="lc-emoji-picker__tab"
|
|
106
|
+
data-lc-emoji-picker-target="tab"
|
|
107
107
|
data-target-section="recent"
|
|
108
|
-
data-action="click->emoji-picker#jumpTo"
|
|
108
|
+
data-action="click->lc-emoji-picker#jumpTo"
|
|
109
109
|
aria-label="<%= lc_t("lostcoords.ui.emoji_picker.recently_used") %>"
|
|
110
110
|
>🕘</button>
|
|
111
111
|
<% end %>
|
|
@@ -113,29 +113,29 @@
|
|
|
113
113
|
<% lc_emoji_picker_groups.each do |group| %>
|
|
114
114
|
<button
|
|
115
115
|
type="button"
|
|
116
|
-
class="emoji-picker__tab"
|
|
117
|
-
data-emoji-picker-target="tab"
|
|
116
|
+
class="lc-emoji-picker__tab"
|
|
117
|
+
data-lc-emoji-picker-target="tab"
|
|
118
118
|
data-target-section="<%= group[:key] %>"
|
|
119
|
-
data-action="click->emoji-picker#jumpTo"
|
|
119
|
+
data-action="click->lc-emoji-picker#jumpTo"
|
|
120
120
|
aria-label="<%= group[:name] %>"
|
|
121
121
|
><%= group[:icon] %></button>
|
|
122
122
|
<% end %>
|
|
123
123
|
</nav>
|
|
124
124
|
|
|
125
|
-
<div class="emoji-picker__body" data-emoji-picker-target="body">
|
|
125
|
+
<div class="lc-emoji-picker__body" data-lc-emoji-picker-target="body" data-action="keydown->lc-emoji-picker#navigateGrid focusin->lc-emoji-picker#trackGridFocus">
|
|
126
126
|
<% if recent.any? %>
|
|
127
127
|
<section
|
|
128
|
-
class="emoji-picker__recent"
|
|
128
|
+
class="lc-emoji-picker__recent"
|
|
129
129
|
data-section="recent"
|
|
130
130
|
aria-label="<%= lc_t('lostcoords.ui.emoji_picker.recently_used') %>"
|
|
131
|
-
data-emoji-picker-target="recent"
|
|
131
|
+
data-lc-emoji-picker-target="recent"
|
|
132
132
|
>
|
|
133
|
-
<header class="emoji-picker__category-head">
|
|
133
|
+
<header class="lc-emoji-picker__category-head">
|
|
134
134
|
<h3><%= lc_t("lostcoords.ui.emoji_picker.recently_used") %></h3>
|
|
135
135
|
<span class="count"><%= recent.size %></span>
|
|
136
136
|
</header>
|
|
137
137
|
|
|
138
|
-
<div class="emoji-picker__grid">
|
|
138
|
+
<div class="lc-emoji-picker__grid">
|
|
139
139
|
<% recent.each do |emoji| %>
|
|
140
140
|
<button
|
|
141
141
|
type="button"
|
|
@@ -153,23 +153,23 @@
|
|
|
153
153
|
</section>
|
|
154
154
|
<% end %>
|
|
155
155
|
|
|
156
|
-
<section class="emoji-picker__results" data-emoji-picker-target="results" aria-label="<%= lc_t('lostcoords.ui.emoji_picker.search_results') %>" hidden>
|
|
157
|
-
<header class="emoji-picker__category-head">
|
|
156
|
+
<section class="lc-emoji-picker__results" data-lc-emoji-picker-target="results" aria-label="<%= lc_t('lostcoords.ui.emoji_picker.search_results') %>" hidden>
|
|
157
|
+
<header class="lc-emoji-picker__category-head">
|
|
158
158
|
<h3><%= lc_t("lostcoords.ui.emoji_picker.search_results") %></h3>
|
|
159
|
-
<span class="count" data-emoji-picker-target="resultsCount">0</span>
|
|
159
|
+
<span class="count" data-lc-emoji-picker-target="resultsCount">0</span>
|
|
160
160
|
</header>
|
|
161
161
|
|
|
162
|
-
<div class="emoji-picker__grid" data-emoji-picker-target="resultsGrid"></div>
|
|
162
|
+
<div class="lc-emoji-picker__grid" data-lc-emoji-picker-target="resultsGrid"></div>
|
|
163
163
|
</section>
|
|
164
164
|
|
|
165
|
-
<p class="emoji-picker__empty" data-emoji-picker-target="empty" hidden>
|
|
165
|
+
<p class="lc-emoji-picker__empty" data-lc-emoji-picker-target="empty" hidden>
|
|
166
166
|
<%= lc_t("lostcoords.ui.emoji_picker.no_results") %>
|
|
167
167
|
</p>
|
|
168
168
|
</div>
|
|
169
169
|
|
|
170
|
-
<footer class="emoji-picker__preview" data-emoji-picker-target="preview">
|
|
171
|
-
<span class="emoji-picker__preview-emoji" data-emoji-picker-target="previewEmoji"><%= selected %></span>
|
|
172
|
-
<span class="emoji-picker__preview-name" data-emoji-picker-target="previewName"><%= lc_emoji_picker_search_name(selected) %></span>
|
|
170
|
+
<footer class="lc-emoji-picker__preview" data-lc-emoji-picker-target="preview">
|
|
171
|
+
<span class="lc-emoji-picker__preview-emoji" data-lc-emoji-picker-target="previewEmoji"><%= selected %></span>
|
|
172
|
+
<span class="lc-emoji-picker__preview-name" data-lc-emoji-picker-target="previewName"><%= lc_emoji_picker_search_name(selected) %></span>
|
|
173
173
|
</footer>
|
|
174
174
|
</div>
|
|
175
175
|
</div>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<% lc_emoji_picker_groups.each do |group| %>
|
|
2
2
|
<section
|
|
3
|
-
class="emoji-picker__category"
|
|
3
|
+
class="lc-emoji-picker__category"
|
|
4
4
|
data-section="<%= group[:key] %>"
|
|
5
5
|
aria-label="<%= group[:name] %>"
|
|
6
6
|
>
|
|
7
|
-
<header class="emoji-picker__category-head">
|
|
7
|
+
<header class="lc-emoji-picker__category-head">
|
|
8
8
|
<h3><%= group[:name] %></h3>
|
|
9
9
|
<span class="count"><%= group[:emojis].size %></span>
|
|
10
10
|
</header>
|
|
11
11
|
|
|
12
|
-
<div class="emoji-picker__grid">
|
|
12
|
+
<div class="lc-emoji-picker__grid">
|
|
13
13
|
<% group[:emojis].each do |emoji| %>
|
|
14
14
|
<button
|
|
15
15
|
type="button"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%# locals: (id:, language:, title:, label:, content:, truncated:, max_lines:, attributes:) %>
|
|
2
|
+
<%= tag.div(**attributes) do %>
|
|
3
|
+
<% if title %>
|
|
4
|
+
<div class="flex h-10 items-center ps-inset pe-16 pt-inset">
|
|
5
|
+
<span class="truncate font-mono text-xs text-muted-foreground"><%= title %></span>
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
|
8
|
+
<div class="lc-code-block-copy absolute end-2 top-2 z-raised rounded-full bg-muted">
|
|
9
|
+
<%= lc_copy_button source: "#{id}-content", icon_only: true, variant: :ghost, size: :sm, aria: { label: lc_t("lostcoords.ui.code_block.copy", label: label) } %>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="relative">
|
|
12
|
+
<pre id="<%= id %>-content" class="m-0 overflow-x-auto ps-inset pe-16 pb-inset font-mono text-[0.8125rem] text-muted-foreground [tab-size:2] <%= title ? 'pt-2 [--lc-code-block-padding:--spacing(7)]' : 'pt-inset [--lc-code-block-padding:--spacing(10)]' %> <%= 'max-h-[calc(var(--lc-code-block-lines)*1lh+var(--lc-code-block-padding))] overflow-y-hidden group-data-expanded/code-block:max-h-none' if truncated %>" style="<%= "--lc-code-block-lines: #{max_lines}" if truncated %>" tabindex="0" role="region" aria-label="<%= label %>"><%= tag.code(content, data: { language: language }) %></pre>
|
|
13
|
+
<% if truncated %><div class="pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-linear-to-t from-muted to-transparent group-data-expanded/code-block:hidden" aria-hidden="true"></div><% end %>
|
|
14
|
+
</div>
|
|
15
|
+
<% if truncated %>
|
|
16
|
+
<%= lc_button lc_t("lostcoords.ui.code_block.expand"), variant: :ghost, size: :sm, class: "w-full rounded-none", aria: { expanded: false, controls: "#{id}-content" }, data: { action: "lc-code-block#toggle", lc_code_block_target: "toggle", expand_label: lc_t("lostcoords.ui.code_block.expand"), collapse_label: lc_t("lostcoords.ui.code_block.collapse") } %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
<%# locals: (id:, title:, trigger:, attributes:, content:) %>
|
|
2
|
-
<div data-controller="
|
|
3
|
-
<button type="button" class="<%= lc_button_class(variant: :secondary) %>" aria-haspopup="dialog" aria-controls="<%= id %>" data-action="
|
|
1
|
+
<%# locals: (id:, title:, description:, trigger:, attributes:, content:) %>
|
|
2
|
+
<div data-controller="lc-dialog">
|
|
3
|
+
<button type="button" class="<%= lc_button_class(variant: :secondary) %>" aria-haspopup="dialog" aria-controls="<%= id %>" data-action="lc-dialog#open" data-slot="dialog-trigger"><%= trigger %></button>
|
|
4
4
|
<%= tag.dialog(**attributes) do %>
|
|
5
|
-
<div class="flex flex-col gap-
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
<div class="flex flex-col gap-inset p-inset" data-slot="dialog-content">
|
|
6
|
+
<header class="flex items-start justify-between gap-content" data-slot="dialog-header">
|
|
7
|
+
<div class="flex min-w-0 flex-col gap-tight">
|
|
8
|
+
<h2 id="<%= id %>-title" class="<%= lc_heading_class(size: :subheading) %>" data-slot="dialog-title"><%= title %></h2>
|
|
9
|
+
<% if description.present? %><p id="<%= id %>-description" class="<%= lc_text_class(size: :small, tone: :muted) %>" data-slot="dialog-description"><%= description %></p><% end %>
|
|
10
|
+
</div>
|
|
11
|
+
<button type="button" class="<%= lc_icon_button_class(size: :sm, shape: :circle, extra: "-me-related -mt-tight") %>" aria-label="<%= lc_t("lostcoords.ui.dialog.close", title: title) %>" data-action="lc-dialog#close"><%= lc_icon "x-mark", options: { "aria-hidden": true } %></button>
|
|
12
|
+
</header>
|
|
10
13
|
<%= content %>
|
|
11
14
|
</div>
|
|
12
15
|
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# locals: (id:, label:, title:, controller:, role:, trigger_variant:, attributes:, content:) %>
|
|
2
|
+
<%= tag.div(class: "relative inline-flex", data: { controller: controller }) do %>
|
|
3
|
+
<%= lc_button label, variant: trigger_variant, id: "#{id}-trigger", aria: { haspopup: role, expanded: false, controls: id }, data: { "#{controller}_target" => "trigger", action: "#{controller}#toggle" } %>
|
|
4
|
+
<%= tag.div(**attributes) do %>
|
|
5
|
+
<% if title %><h2 id="<%= id %>-title" class="<%= lc_heading_class(size: :subheading) %>"><%= title %></h2><% end %>
|
|
6
|
+
<%= content %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end %>
|
data/config/importmap.rb
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Everything lives under lostcoords/ui/*. Controllers are also pinned under the host's controllers/* key,
|
|
2
|
+
# so eagerLoadControllersFrom("controllers", application) registers them as lc-* identifiers. A host file
|
|
3
|
+
# with the same name, such as app/javascript/controllers/lc_dialog_controller.js, replaces the gem's.
|
|
4
|
+
pin "lostcoords/ui", to: "lostcoords/ui/index.js", preload: false
|
|
2
5
|
pin_all_from Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/controllers"), under: "controllers", to: "lostcoords/ui/controllers", preload: false
|
|
3
|
-
pin_all_from Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/helpers"), under: "helpers", to: "lostcoords/ui/helpers", preload: false
|
|
6
|
+
pin_all_from Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/helpers"), under: "lostcoords/ui/helpers", to: "lostcoords/ui/helpers", preload: false
|
|
7
|
+
# pin_all_from keeps one mapping per directory, so the namespaced controller keys are pinned one by one.
|
|
8
|
+
Dir[Lostcoords::Ui::Engine.root.join("app/javascript/lostcoords/ui/controllers/*.js")].each do |path|
|
|
9
|
+
name = File.basename(path, ".js")
|
|
10
|
+
pin "lostcoords/ui/controllers/#{name}", to: "lostcoords/ui/controllers/#{name}.js", preload: false
|
|
11
|
+
end
|