lostcoords-ui 0.5.0 → 0.7.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 +111 -49
- data/app/assets/tailwind/lostcoords/ui/card.css +5 -2
- data/app/assets/tailwind/lostcoords/ui/{disclosure.css → collapsible.css} +1 -1
- 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 -25
- data/app/assets/tailwind/lostcoords/ui/core.css +21 -0
- data/app/assets/tailwind/lostcoords/ui/emoji-picker.css +45 -47
- data/app/assets/tailwind/lostcoords/ui/popover.css +0 -52
- data/app/assets/tailwind/lostcoords/ui/{reading.css → prose.css} +6 -0
- data/app/assets/tailwind/lostcoords/ui/sheet.css +3 -64
- data/app/assets/tailwind/lostcoords/ui/theme.css +16 -1
- data/app/assets/tailwind/lostcoords/ui/toast.css +23 -0
- data/app/assets/tailwind/lostcoords/ui/tooltip.css +12 -11
- data/app/assets/tailwind/lostcoords/ui/typography.css +1 -7
- data/app/helpers/lostcoords/ui/badge_helper.rb +2 -2
- data/app/helpers/lostcoords/ui/button_helper.rb +12 -6
- data/app/helpers/lostcoords/ui/choice_helper.rb +11 -7
- data/app/helpers/lostcoords/ui/collapsible_helper.rb +9 -0
- data/app/helpers/lostcoords/ui/dialog_helper.rb +10 -5
- data/app/helpers/lostcoords/ui/emoji_picker_helper.rb +6 -0
- data/app/helpers/lostcoords/ui/feedback_helper.rb +2 -2
- data/app/helpers/lostcoords/ui/form_helper.rb +4 -3
- data/app/helpers/lostcoords/ui/glass_helper.rb +1 -1
- data/app/helpers/lostcoords/ui/menu_helper.rb +37 -2
- data/app/helpers/lostcoords/ui/navigation_helper.rb +17 -4
- data/app/helpers/lostcoords/ui/render_attributes_helper.rb +17 -1
- data/app/helpers/lostcoords/ui/sheet_helper.rb +4 -0
- data/app/helpers/lostcoords/ui/structures_helper.rb +7 -95
- data/app/helpers/lostcoords/ui/toast_helper.rb +22 -0
- data/app/helpers/lostcoords/ui/{overlay_helper.rb → tooltip_helper.rb} +3 -3
- data/app/helpers/lostcoords/ui/typography_helper.rb +30 -0
- data/app/javascript/lostcoords/ui/controllers/lc_collapsible_controller.js +1 -1
- data/app/javascript/lostcoords/ui/controllers/lc_emoji_picker_controller.js +34 -6
- data/app/javascript/lostcoords/ui/controllers/lc_kbd_controller.js +12 -0
- data/app/javascript/lostcoords/ui/controllers/lc_menu_controller.js +23 -1
- data/app/javascript/lostcoords/ui/controllers/lc_selection_controller.js +8 -16
- data/app/javascript/lostcoords/ui/controllers/lc_sheet_controller.js +15 -3
- data/app/javascript/lostcoords/ui/controllers/lc_shortcut_controller.js +45 -0
- data/app/javascript/lostcoords/ui/controllers/lc_tabs_controller.js +18 -15
- data/app/javascript/lostcoords/ui/controllers/lc_toast_region_controller.js +62 -0
- data/app/javascript/lostcoords/ui/controllers/lc_tooltip_controller.js +46 -6
- data/app/javascript/lostcoords/ui/helpers/catalog_picker.js +72 -20
- data/app/javascript/lostcoords/ui/helpers/dismiss_stack.js +23 -0
- data/app/javascript/lostcoords/ui/helpers/floating_panel.js +6 -7
- 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/shortcuts.js +162 -0
- data/app/javascript/lostcoords/ui/helpers/top_layer.js +48 -10
- data/app/javascript/lostcoords/ui/index.js +6 -0
- data/app/views/lostcoords/ui/components/_emoji_picker.html.erb +27 -27
- data/app/views/lostcoords/ui/components/_emoji_picker_grid.html.erb +3 -3
- data/app/views/lostcoords/ui/components/_lc_code_block.html.erb +1 -1
- data/lib/lostcoords/ui/action_view.rb +1 -0
- data/lib/lostcoords/ui/builders.rb +2 -2
- data/lib/lostcoords/ui/form_builder.rb +55 -10
- data/lib/lostcoords/ui/stylesheets.rb +25 -0
- data/lib/lostcoords/ui/version.rb +1 -1
- data/lib/tasks/lostcoords_ui.rake +11 -0
- metadata +23 -6
- data/config/helper_renames.json +0 -111
- data/script/migrate_helpers.rb +0 -136
|
@@ -6,7 +6,7 @@ module Lostcoords::Ui
|
|
|
6
6
|
CHOICE_LABEL = "lc-choice inline-flex min-w-0 items-start gap-related text-body text-foreground cursor-pointer has-disabled:cursor-not-allowed has-disabled:opacity-60"
|
|
7
7
|
|
|
8
8
|
# card: true draws the whole label as a selectable tile, for plan or theme pickers.
|
|
9
|
-
CHOICE_CARD = "flex w-full rounded-2xl lc-squircle border border-transparent bg-
|
|
9
|
+
CHOICE_CARD = "flex w-full rounded-2xl lc-squircle border border-transparent bg-tile p-content " \
|
|
10
10
|
"transition-[background-color,border-color] duration-(--lc-motion-quick) ease-(--lc-ease-out) hover:bg-accent " \
|
|
11
11
|
"has-checked:border-primary has-checked:bg-primary-soft has-focus-visible:outline-2 has-focus-visible:outline-offset-2 " \
|
|
12
12
|
"has-focus-visible:outline-(--ring,color-mix(in_oklab,var(--primary)_50%,transparent))"
|
|
@@ -42,15 +42,18 @@ module Lostcoords::Ui
|
|
|
42
42
|
lc_choice("switch", label, hint, false, name, unchecked_value, { type: "checkbox", role: "switch", name: name, value: value, checked: checked, class: lc_switch_class }, attributes)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# A fieldset of checkboxes, radios or switches with a legend, hint and error.
|
|
46
|
-
|
|
45
|
+
# A fieldset of checkboxes, radios or switches with a legend, hint and error. With an id, the
|
|
46
|
+
# fieldset's aria-describedby points at the hint and error, as lc_field does for its control.
|
|
47
|
+
def lc_choice_group(legend:, id: nil, hint: nil, error: nil, **attributes, &block)
|
|
47
48
|
lc_label!(legend, :legend)
|
|
48
49
|
errors = Array(error).compact_blank
|
|
49
|
-
|
|
50
|
+
described = [ ("#{id}-hint" if id && hint.present?), ("#{id}-error" if id && errors.any?) ].compact.presence&.join(" ")
|
|
51
|
+
defaults = { id: id, class: lc_field_class(extra: "m-0 min-w-0 border-0 p-0"), aria: { invalid: (true if errors.any?), describedby: described }, data: { slot: "choice-group", invalid: (true if errors.any?) } }
|
|
52
|
+
tag.fieldset(**lc_attributes(defaults, attributes)) do
|
|
50
53
|
safe_join([
|
|
51
|
-
tag.legend(legend, class: lc_field_label_class(extra: "p-0")),
|
|
54
|
+
tag.legend(legend, class: lc_field_label_class(extra: "p-0"), data: { slot: "choice-group-legend" }),
|
|
52
55
|
tag.div(class: "flex flex-col gap-related", &block),
|
|
53
|
-
*lc_field_messages(
|
|
56
|
+
*lc_field_messages(id, hint, errors)
|
|
54
57
|
])
|
|
55
58
|
end
|
|
56
59
|
end
|
|
@@ -64,7 +67,7 @@ module Lostcoords::Ui
|
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
# Shared with Lostcoords::Ui::FormBuilder, which renders its own inputs.
|
|
67
|
-
def lc_choice_label(slot, label, hint, input, card: false, **attributes, &block)
|
|
70
|
+
def lc_choice_label(slot, label, hint, input, card: false, errors: [], error_id: nil, **attributes, &block)
|
|
68
71
|
extra = capture(&block) if block
|
|
69
72
|
tag.label(**lc_attributes({ class: lc_choice_label_class(card: card), data: { slot: slot, card: (true if card) } }, attributes)) do
|
|
70
73
|
safe_join([
|
|
@@ -73,6 +76,7 @@ module Lostcoords::Ui
|
|
|
73
76
|
safe_join([
|
|
74
77
|
tag.span(label, class: ("text-label" if card)),
|
|
75
78
|
(tag.span(hint, class: "text-small text-muted-foreground") if hint.present?),
|
|
79
|
+
(tag.span(safe_join(errors, tag.br), id: error_id, class: "text-small text-destructive", data: { slot: "field-error" }) if errors.any?),
|
|
76
80
|
(tag.span(extra, class: "flex min-w-0 flex-col pt-tight") if extra.present?)
|
|
77
81
|
].compact)
|
|
78
82
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module CollapsibleHelper
|
|
3
|
+
def lc_collapsible(title:, open: false, **attributes, &block)
|
|
4
|
+
lc_label!(title, :title)
|
|
5
|
+
defaults = { class: lc_card_class(extra: "lc-collapsible"), open: open, data: { slot: "collapsible", controller: "lc-collapsible", action: "click->lc-collapsible#toggle" } }
|
|
6
|
+
render "lostcoords/ui/components/lc_collapsible", title: title, attributes: lc_attributes(defaults, attributes), content: (capture(&block) if block)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -14,13 +14,18 @@ module Lostcoords::Ui
|
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
16
|
def lc_dialog_class(kind = :modal, extra: nil)
|
|
17
|
-
lc_tw("lc-squircle",
|
|
17
|
+
lc_tw("lc-squircle", DIALOGS.fetch(lc_option!(:kind, kind, DIALOGS.keys)), extra)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
# Yields a builder with footer and close parts.
|
|
21
|
+
def lc_dialog(id:, title:, description: nil, trigger: lc_t("lostcoords.ui.dialog.trigger"), kind: :modal, detents: "fit", **attributes, &block)
|
|
22
|
+
lc_label!(id, :id)
|
|
23
|
+
lc_label!(title, :title)
|
|
24
|
+
lc_label!(trigger, :trigger)
|
|
25
|
+
defaults = { class: lc_dialog_class(kind), aria: { describedby: ("#{id}-description" if description.present?) }, data: lc_sheet_data({ slot: "dialog" }, detents: detents) }
|
|
26
|
+
required = { id: id, aria: { labelledby: "#{id}-title" }, data: { lc_dialog_target: "dialog" } }
|
|
27
|
+
render "lostcoords/ui/components/lc_dialog", id: id, title: title, description: description, trigger: trigger,
|
|
28
|
+
attributes: lc_attributes(defaults, attributes, required), content: (capture(Parts::Dialog.new(self, id), &block) if block)
|
|
24
29
|
end
|
|
25
30
|
end
|
|
26
31
|
end
|
|
@@ -6,6 +6,12 @@ module Lostcoords::Ui
|
|
|
6
6
|
module EmojiPickerHelper
|
|
7
7
|
include TranslationHelper
|
|
8
8
|
|
|
9
|
+
def lc_emoji_picker(form:, field:, label:, catalog_url:, value: nil, quick: [], recent: [], recent_scope: nil, auto_submit: false, **attributes)
|
|
10
|
+
lc_label!(label, :label)
|
|
11
|
+
lc_label!(catalog_url, :catalog_url)
|
|
12
|
+
render "lostcoords/ui/components/emoji_picker", form: form, field: field, label: label, catalog_url: catalog_url, value: value, quick: quick, recent: recent, recent_scope: recent_scope, auto_submit: auto_submit, attributes: attributes
|
|
13
|
+
end
|
|
14
|
+
|
|
9
15
|
SEARCH_NAMES_PATH = Lostcoords::Ui.root.join("config/emoji_search_names.yml")
|
|
10
16
|
SEARCH_NAMES_JA_PATH = Lostcoords::Ui.root.join("config/emoji_search_names_ja.yml")
|
|
11
17
|
SEARCH_ALIASES_PATH = Lostcoords::Ui.root.join("config/emoji_search_aliases.yml")
|
|
@@ -9,13 +9,13 @@ module Lostcoords::Ui
|
|
|
9
9
|
lg: "size-8 border-[3px]"
|
|
10
10
|
}.freeze
|
|
11
11
|
|
|
12
|
-
SKELETON_BASE = "block rounded-xl bg-
|
|
12
|
+
SKELETON_BASE = "block rounded-xl bg-tile animate-pulse"
|
|
13
13
|
|
|
14
14
|
EMPTY_STATE_BASE =
|
|
15
15
|
"flex flex-col items-center gap-content text-center " \
|
|
16
16
|
"py-section px-content"
|
|
17
17
|
|
|
18
|
-
ALERT_BASE = "flex items-start gap-related py-heading px-content rounded-2xl lc-squircle bg-
|
|
18
|
+
ALERT_BASE = "flex items-start gap-related py-heading px-content rounded-2xl lc-squircle bg-tile text-foreground text-label"
|
|
19
19
|
ALERT_ICONS = {
|
|
20
20
|
neutral: [ "information-circle", "text-muted-foreground" ],
|
|
21
21
|
primary: [ "information-circle", "text-primary" ],
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
module Lostcoords::Ui
|
|
2
2
|
module FormHelper
|
|
3
3
|
TOGGLE_GROUP_BASE =
|
|
4
|
-
"inline-flex items-center gap-1 rounded-2xl bg-
|
|
4
|
+
"inline-flex items-center gap-1 rounded-2xl bg-track p-1 lc-squircle"
|
|
5
5
|
|
|
6
6
|
TOGGLE_GROUP_ITEM_BASE =
|
|
7
7
|
"lc-press inline-flex items-center justify-center gap-1.5 whitespace-nowrap " \
|
|
8
8
|
"h-control-sm rounded-xl border-0 bg-transparent " \
|
|
9
9
|
"px-3 font-sans text-label cursor-pointer no-underline " \
|
|
10
|
-
"text-muted-foreground hover:text-foreground " \
|
|
10
|
+
"text-muted-foreground not-disabled:hover:text-foreground " \
|
|
11
11
|
"aria-selected:bg-background aria-selected:text-foreground " \
|
|
12
|
+
"aria-checked:bg-background aria-checked:text-foreground " \
|
|
12
13
|
"aria-pressed:bg-background aria-pressed:text-foreground " \
|
|
13
14
|
"aria-[current]:not-aria-[current=false]:bg-background aria-[current]:not-aria-[current=false]:text-foreground " \
|
|
14
15
|
"transition-[background-color,color,box-shadow,transform] duration-(--lc-motion-quick) ease-(--lc-ease-out) " \
|
|
15
|
-
"disabled:
|
|
16
|
+
"disabled:cursor-not-allowed disabled:text-[color-mix(in_oklab,var(--foreground)_38%,transparent)] " \
|
|
16
17
|
"[&_svg]:size-[1.1em] [&_svg]:shrink-0"
|
|
17
18
|
|
|
18
19
|
TOGGLE_GROUP_ITEM_ACTIVE = "bg-background text-foreground hover:text-foreground"
|
|
@@ -19,7 +19,7 @@ module Lostcoords::Ui
|
|
|
19
19
|
"dark:max-md:[--lc-glass-fill:color-mix(in_oklab,var(--muted)_92%,transparent)]"
|
|
20
20
|
|
|
21
21
|
def lc_glass_class(shape: :bar, thick: false, extra: nil)
|
|
22
|
-
lc_tw("lc-glass", GLASS_SHAPES.fetch(shape
|
|
22
|
+
lc_tw("lc-glass", GLASS_SHAPES.fetch(lc_option!(:shape, shape, GLASS_SHAPES.keys)), (GLASS_THICK if thick), extra)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -27,7 +27,7 @@ module Lostcoords::Ui
|
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
29
|
def lc_popover_class(align: :end, extra: nil)
|
|
30
|
-
lc_glass_class(shape: :menu, extra: lc_tw(POPOVER_BASE, MENU_POPOVER_ALIGN.fetch(align), extra))
|
|
30
|
+
lc_glass_class(shape: :menu, extra: lc_tw(POPOVER_BASE, MENU_POPOVER_ALIGN.fetch(lc_option!(:align, align, MENU_POPOVER_ALIGN.keys)), extra))
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def lc_menu_popover_class(align: :end, extra: nil)
|
|
@@ -35,7 +35,42 @@ module Lostcoords::Ui
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def lc_menu_option_class(tone: :default, extra: nil)
|
|
38
|
-
lc_tw(MENU_OPTION_BASE, MENU_OPTION_TONES.fetch(tone), extra)
|
|
38
|
+
lc_tw(MENU_OPTION_BASE, MENU_OPTION_TONES.fetch(lc_option!(:tone, tone, MENU_OPTION_TONES.keys)), extra)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def lc_menu(id:, label: lc_t("lostcoords.ui.menu.label"), align: :end, **attributes, &block)
|
|
42
|
+
align = lc_option!(:align, align, MENU_POPOVER_ALIGN.keys)
|
|
43
|
+
lc_render_panel(id, label, nil, "lc-menu", "menu", lc_menu_popover_class(align: align), lc_attributes({ data: { slot: "menu" } }, attributes), &block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def lc_popover(id:, label: lc_t("lostcoords.ui.popover.trigger"), title: nil, align: :end, trigger_variant: :secondary, **attributes, &block)
|
|
47
|
+
title = title.presence
|
|
48
|
+
align = lc_option!(:align, align, MENU_POPOVER_ALIGN.keys)
|
|
49
|
+
classes = lc_popover_class(align: align, extra: "data-open:flex flex-col gap-content p-inset z-(--lc-z-popover) min-w-64 max-w-[calc(100vw-2rem)]")
|
|
50
|
+
lc_render_panel(id, label, title, "lc-popover", "dialog", classes, lc_attributes({ data: { slot: "popover" } }, attributes), trigger_variant: trigger_variant, &block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def lc_menu_option(label = nil, href: nil, tone: :default, **attributes, &block)
|
|
54
|
+
tone = lc_option!(:tone, tone, MENU_OPTION_TONES.keys)
|
|
55
|
+
content = tag.span(block ? capture(&block) : label, class: "col-span-3")
|
|
56
|
+
defaults = { class: lc_menu_option_class(tone: tone), role: "menuitem", data: { slot: "menu-item", action: "click->lc-menu#close" } }
|
|
57
|
+
if href
|
|
58
|
+
raise ArgumentError, "Links cannot be disabled; use a menu button instead" if attributes[:disabled] || attributes["disabled"]
|
|
59
|
+
|
|
60
|
+
tag.a(content, **lc_attributes(defaults.merge(href: href), attributes))
|
|
61
|
+
else
|
|
62
|
+
tag.button(content, **lc_attributes(defaults.merge(type: "button"), attributes))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
def lc_render_panel(id, label, title, controller, role, classes, attributes, trigger_variant: :secondary, &block)
|
|
68
|
+
lc_label!(id, :id)
|
|
69
|
+
lc_label!(label, :label)
|
|
70
|
+
required = { id: id, role: role, aria: { labelledby: "#{id}-#{title ? 'title' : 'trigger'}" }, data: { "#{controller}_target" => "popover" } }
|
|
71
|
+
required[:tabindex] = -1 if role == "dialog"
|
|
72
|
+
merged = lc_attributes({ class: classes }, attributes, required)
|
|
73
|
+
render "lostcoords/ui/components/lc_floating_panel", id: id, label: label, title: title, controller: controller, role: role, trigger_variant: trigger_variant, attributes: merged, content: (capture(&block) if block)
|
|
39
74
|
end
|
|
40
75
|
end
|
|
41
76
|
end
|
|
@@ -99,10 +99,20 @@ module Lostcoords::Ui
|
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
TABS_ORIENTATIONS = {
|
|
103
|
+
horizontal: { root: "flex min-w-0 flex-col gap-content", list: "max-w-full overflow-x-auto lc-scroll-fade-x" },
|
|
104
|
+
vertical: { root: "flex min-w-0 flex-row items-start gap-content", list: "shrink-0 flex-col items-stretch [&>[role=tab]]:justify-start" }
|
|
105
|
+
}.freeze
|
|
106
|
+
|
|
102
107
|
# Yields a builder; each `tabs.panel "Label" do … end` adds a tab and its panel.
|
|
103
|
-
|
|
108
|
+
# orientation: :vertical stacks the tabs beside the panels and moves them with Arrow Up and Down.
|
|
109
|
+
# activation: :manual moves focus with the arrows and selects with Enter or Space.
|
|
110
|
+
def lc_tabs(id:, label:, selected: nil, orientation: :horizontal, activation: :automatic, **attributes, &block)
|
|
104
111
|
lc_label!(id, :id)
|
|
105
112
|
lc_label!(label, :label)
|
|
113
|
+
orientation = lc_option!(:orientation, orientation, TABS_ORIENTATIONS.keys)
|
|
114
|
+
activation = lc_option!(:activation, activation, %i[automatic manual])
|
|
115
|
+
layout = TABS_ORIENTATIONS.fetch(orientation)
|
|
106
116
|
tabs = Parts::Tabs.new(self)
|
|
107
117
|
capture(tabs, &block) if block
|
|
108
118
|
raise ArgumentError, "lc_tabs needs at least one panel" if tabs.panels.empty?
|
|
@@ -111,7 +121,9 @@ module Lostcoords::Ui
|
|
|
111
121
|
raise ArgumentError, "selected tab #{selected.inspect} has no panel" unless tabs.panels.any? { |panel| panel.value == selected }
|
|
112
122
|
|
|
113
123
|
key = ->(panel) { "#{id}-#{panel.value.gsub(/[^\w-]+/, '-')}" }
|
|
114
|
-
|
|
124
|
+
ids = tabs.panels.map(&key)
|
|
125
|
+
raise ArgumentError, "tab values #{tabs.panels.map(&:value).inspect} produce duplicate ids" unless ids.uniq.size == ids.size
|
|
126
|
+
list = tag.div(role: "tablist", aria: { label: label, orientation: (orientation if orientation == :vertical) }, class: lc_toggle_group_class(extra: layout[:list]), data: { slot: "tabs-list" }) do
|
|
115
127
|
safe_join(tabs.panels.map do |panel|
|
|
116
128
|
active = panel.value == selected
|
|
117
129
|
tag.button(panel.label, type: "button", role: "tab", id: "#{key.(panel)}-tab", tabindex: (active ? 0 : -1),
|
|
@@ -121,9 +133,10 @@ module Lostcoords::Ui
|
|
|
121
133
|
end
|
|
122
134
|
panels = tabs.panels.map do |panel|
|
|
123
135
|
tag.div(panel.content, **lc_attributes({ role: "tabpanel", id: "#{key.(panel)}-panel", tabindex: 0, hidden: (panel.value != selected),
|
|
124
|
-
aria: { labelledby: "#{key.(panel)}-tab" }, class: "min-w-0 rounded-lg outline-offset-4", data: { slot: "tabs-panel", lc_tabs_target: "panel" } }, panel.attributes))
|
|
136
|
+
aria: { labelledby: "#{key.(panel)}-tab" }, class: lc_tw("min-w-0 rounded-lg outline-offset-4", ("flex-1" if orientation == :vertical)), data: { slot: "tabs-panel", lc_tabs_target: "panel" } }, panel.attributes))
|
|
125
137
|
end
|
|
126
|
-
|
|
138
|
+
data = { slot: "tabs", controller: "lc-tabs", lc_tabs_activation_value: (activation.to_s if activation == :manual) }.compact
|
|
139
|
+
tag.div(**lc_attributes({ id: id, class: layout[:root], data: data }, attributes)) do
|
|
127
140
|
safe_join([ list, *panels ])
|
|
128
141
|
end
|
|
129
142
|
end
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
module Lostcoords::Ui
|
|
2
2
|
module RenderAttributesHelper
|
|
3
3
|
private
|
|
4
|
-
|
|
4
|
+
# required: attributes the component needs to work, merged last so a caller can't override them.
|
|
5
|
+
def lc_attributes(defaults, attributes, required = nil)
|
|
6
|
+
return lc_attributes(lc_attributes(defaults, attributes), required) if required
|
|
7
|
+
|
|
5
8
|
defaults = lc_normalize_attributes(defaults)
|
|
6
9
|
attributes = lc_normalize_attributes(attributes)
|
|
7
10
|
merged = defaults.merge(attributes)
|
|
@@ -36,6 +39,19 @@ module Lostcoords::Ui
|
|
|
36
39
|
normalized
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
def lc_label!(value, name)
|
|
43
|
+
raise ArgumentError, "#{name} must not be blank" if value.blank?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The hint and error paragraphs under a field or choice group. With an id, controls can point
|
|
47
|
+
# aria-describedby at them.
|
|
48
|
+
def lc_field_messages(id, hint, errors)
|
|
49
|
+
[
|
|
50
|
+
(tag.p(hint, id: ("#{id}-hint" if id), class: "m-0 text-small text-muted-foreground", data: { slot: "field-description" }) if hint.present?),
|
|
51
|
+
(tag.p(safe_join(errors, tag.br), id: ("#{id}-error" if id), class: "m-0 text-small text-destructive", role: "alert", data: { slot: "field-error" }) if errors.any?)
|
|
52
|
+
].compact
|
|
53
|
+
end
|
|
54
|
+
|
|
39
55
|
def lc_option!(name, value, allowed)
|
|
40
56
|
normalized = value.to_sym if value.is_a?(String) || value.is_a?(Symbol)
|
|
41
57
|
return normalized if allowed.include?(normalized)
|
|
@@ -2,7 +2,11 @@ module Lostcoords::Ui
|
|
|
2
2
|
module SheetHelper
|
|
3
3
|
# Makes a <dialog> an iOS sheet on phones (lc-sheet controller), keeping the dialog's own
|
|
4
4
|
# controllers. detents: "fit", "medium large" or "large".
|
|
5
|
+
SHEET_DETENTS = [ "fit", "medium large", "large" ].freeze
|
|
6
|
+
|
|
5
7
|
def lc_sheet_data(data = {}, detents: "fit")
|
|
8
|
+
detents = lc_option!(:detents, detents, SHEET_DETENTS.map(&:to_sym)).to_s
|
|
9
|
+
data = data.to_h.symbolize_keys
|
|
6
10
|
data.merge(
|
|
7
11
|
controller: [ data[:controller], "lc-sheet" ].compact.join(" "),
|
|
8
12
|
lc_sheet_detents_value: detents
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module Lostcoords::Ui
|
|
2
2
|
module StructuresHelper
|
|
3
3
|
INPUT_TYPES = %i[text search email password tel url number date time datetime-local month week].freeze
|
|
4
|
-
SHEET_DETENTS = [ "fit", "medium large", "large" ].freeze
|
|
5
4
|
|
|
6
5
|
def lc_input(name: nil, value: nil, type: :text, size: :md, **attributes)
|
|
7
6
|
type = lc_option!(:type, type, INPUT_TYPES)
|
|
@@ -36,10 +35,12 @@ module Lostcoords::Ui
|
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
|
|
38
|
+
# One choice from a few options, as a segmented control. Arrows move and select, like native radios,
|
|
39
|
+
# so it is a radiogroup of radio buttons to assistive technology.
|
|
39
40
|
def lc_toggle_group(label:, name: nil, value: nil, **attributes, &block)
|
|
40
41
|
lc_label!(label, :label)
|
|
41
|
-
defaults = { class: lc_toggle_group_class, role: "
|
|
42
|
-
tag.div(**lc_attributes(
|
|
42
|
+
defaults = { class: lc_toggle_group_class, role: "radiogroup", aria: { label: label }, data: { slot: "toggle-group", controller: "lc-selection", action: "click->lc-selection#select keydown->lc-selection#navigate", lc_selection_value_value: value } }
|
|
43
|
+
tag.div(**lc_attributes(defaults, attributes, { role: "radiogroup", aria: { label: label } })) do
|
|
43
44
|
safe_join([ (hidden_field_tag(name, value, data: { lc_selection_target: "input" }) if name), (capture(&block) if block) ].compact)
|
|
44
45
|
end
|
|
45
46
|
end
|
|
@@ -47,9 +48,9 @@ module Lostcoords::Ui
|
|
|
47
48
|
def lc_toggle_group_item(label = nil, value: nil, selected: false, **attributes, &block)
|
|
48
49
|
value ||= label
|
|
49
50
|
lc_label!(value, :value)
|
|
50
|
-
defaults = { class: lc_toggle_group_item_class,
|
|
51
|
-
required = { type: "button", data: { lc_selection_target: "option", value: value } }
|
|
52
|
-
tag.button(block ? capture(&block) : label, **lc_attributes(
|
|
51
|
+
defaults = { class: lc_toggle_group_item_class, aria: { checked: selected }, data: { slot: "toggle-group-item" } }
|
|
52
|
+
required = { type: "button", role: "radio", data: { lc_selection_target: "option", value: value } }
|
|
53
|
+
tag.button(block ? capture(&block) : label, **lc_attributes(defaults, attributes, required))
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def lc_toggle(label = nil, active: false, ghost: false, size: :md, **attributes, &block)
|
|
@@ -57,94 +58,5 @@ module Lostcoords::Ui
|
|
|
57
58
|
defaults = { class: lc_toggle_class(ghost: ghost, size: size), type: "button", aria: { pressed: active }, data: { slot: "toggle", controller: "lc-toggle", action: "click->lc-toggle#toggle" } }
|
|
58
59
|
tag.button(block ? capture(&block) : label, **lc_attributes(defaults, attributes).merge(type: "button"))
|
|
59
60
|
end
|
|
60
|
-
|
|
61
|
-
# Yields a builder with footer and close parts.
|
|
62
|
-
def lc_dialog(id:, title:, description: nil, trigger: lc_t("lostcoords.ui.dialog.trigger"), kind: :modal, detents: "fit", **attributes, &block)
|
|
63
|
-
kind = lc_option!(:kind, kind, DialogHelper::DIALOGS.keys)
|
|
64
|
-
detents = detents.to_s
|
|
65
|
-
raise ArgumentError, "detents must be one of #{SHEET_DETENTS.join(', ')}" unless SHEET_DETENTS.include?(detents)
|
|
66
|
-
defaults = { class: lc_dialog_class(kind), aria: { describedby: ("#{id}-description" if description.present?) }, data: lc_sheet_data({ slot: "dialog" }, detents: detents) }
|
|
67
|
-
lc_render_dialog(id, title, description, trigger, defaults, attributes, &block)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def lc_menu(id:, label: lc_t("lostcoords.ui.menu.label"), align: :end, **attributes, &block)
|
|
71
|
-
align = lc_option!(:align, align, MenuHelper::MENU_POPOVER_ALIGN.keys)
|
|
72
|
-
lc_render_panel(id, label, nil, "lc-menu", "menu", lc_menu_popover_class(align: align), lc_attributes({ data: { slot: "menu" } }, attributes), &block)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def lc_popover(id:, label: lc_t("lostcoords.ui.popover.trigger"), title: nil, align: :end, trigger_variant: :secondary, **attributes, &block)
|
|
76
|
-
title = title.presence
|
|
77
|
-
align = lc_option!(:align, align, MenuHelper::MENU_POPOVER_ALIGN.keys)
|
|
78
|
-
classes = lc_popover_class(align: align, extra: "data-open:flex flex-col gap-content p-inset z-(--lc-z-popover) min-w-64 max-w-[calc(100vw-2rem)]")
|
|
79
|
-
lc_render_panel(id, label, title, "lc-popover", "dialog", classes, lc_attributes({ data: { slot: "popover" } }, attributes), trigger_variant: trigger_variant, &block)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def lc_menu_option(label = nil, href: nil, tone: :default, **attributes, &block)
|
|
83
|
-
tone = lc_option!(:tone, tone, MenuHelper::MENU_OPTION_TONES.keys)
|
|
84
|
-
content = tag.span(block ? capture(&block) : label, class: "col-span-3")
|
|
85
|
-
defaults = { class: lc_menu_option_class(tone: tone), role: "menuitem", data: { slot: "menu-item", action: "click->lc-menu#close" } }
|
|
86
|
-
if href
|
|
87
|
-
raise ArgumentError, "Links cannot be disabled; use a menu button instead" if attributes[:disabled] || attributes["disabled"]
|
|
88
|
-
|
|
89
|
-
tag.a(content, **lc_attributes(defaults.merge(href: href), attributes))
|
|
90
|
-
else
|
|
91
|
-
tag.button(content, **lc_attributes(defaults.merge(type: "button"), attributes))
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def lc_toast(message = nil, tone: :success, timeout: 4000, **attributes, &block)
|
|
96
|
-
tone = lc_option!(:tone, tone, FeedbackHelper::ALERT_ICONS.keys)
|
|
97
|
-
raise ArgumentError, "timeout must be a nonnegative integer" unless timeout.is_a?(Integer) && timeout >= 0
|
|
98
|
-
|
|
99
|
-
defaults = { class: lc_glass_class(shape: :hud, extra: "flex items-center gap-related px-content py-heading text-label font-semibold transition-opacity data-leaving:opacity-0"), role: tone == :destructive ? "alert" : "status", data: { slot: "toast", tone: tone, controller: "lc-toast", action: "click->lc-toast#dismiss", lc_toast_timeout_value: timeout } }
|
|
100
|
-
tag.div(**lc_attributes(defaults, attributes)) do
|
|
101
|
-
safe_join([ lc_alert_icon(tone: tone), tag.span(block ? capture(&block) : message) ])
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def lc_collapsible(title:, open: false, **attributes, &block)
|
|
106
|
-
lc_label!(title, :title)
|
|
107
|
-
defaults = { class: lc_card_class(extra: "lc-collapsible"), open: open, data: { slot: "collapsible", controller: "lc-collapsible", action: "click->lc-collapsible#toggle" } }
|
|
108
|
-
render "lostcoords/ui/components/lc_collapsible", title: title, attributes: lc_attributes(defaults, attributes), content: (capture(&block) if block)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def lc_emoji_picker(form:, field:, label:, catalog_url:, value: nil, quick: [], recent: [], recent_scope: nil, auto_submit: false, **attributes)
|
|
112
|
-
lc_label!(label, :label)
|
|
113
|
-
lc_label!(catalog_url, :catalog_url)
|
|
114
|
-
render "lostcoords/ui/components/emoji_picker", form: form, field: field, label: label, catalog_url: catalog_url, value: value, quick: quick, recent: recent, recent_scope: recent_scope, auto_submit: auto_submit, attributes: attributes
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
private
|
|
118
|
-
# The hint and error paragraphs under a field or choice group. With an id, controls can point
|
|
119
|
-
# aria-describedby at them.
|
|
120
|
-
def lc_field_messages(id, hint, errors)
|
|
121
|
-
[
|
|
122
|
-
(tag.p(hint, id: ("#{id}-hint" if id), class: "m-0 text-small text-muted-foreground", data: { slot: "field-description" }) if hint.present?),
|
|
123
|
-
(tag.p(safe_join(errors, tag.br), id: ("#{id}-error" if id), class: "m-0 text-small text-destructive", role: "alert", data: { slot: "field-error" }) if errors.any?)
|
|
124
|
-
].compact
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def lc_label!(value, name)
|
|
129
|
-
raise ArgumentError, "#{name} must not be blank" if value.blank?
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def lc_render_dialog(id, title, description, trigger, defaults, attributes, &block)
|
|
133
|
-
lc_label!(id, :id)
|
|
134
|
-
lc_label!(title, :title)
|
|
135
|
-
lc_label!(trigger, :trigger)
|
|
136
|
-
required = { id: id, aria: { labelledby: "#{id}-title" }, data: (defaults[:data] || {}).merge(lc_dialog_target: "dialog") }
|
|
137
|
-
merged = lc_attributes(lc_attributes(defaults, attributes), required)
|
|
138
|
-
render "lostcoords/ui/components/lc_dialog", id: id, title: title, description: description, trigger: trigger, attributes: merged, content: (capture(Parts::Dialog.new(self, id), &block) if block)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def lc_render_panel(id, label, title, controller, role, classes, attributes, trigger_variant: :secondary, &block)
|
|
142
|
-
lc_label!(id, :id)
|
|
143
|
-
lc_label!(label, :label)
|
|
144
|
-
required = { id: id, role: role, aria: { labelledby: "#{id}-#{title ? 'title' : 'trigger'}" }, data: { "#{controller}_target" => "popover" } }
|
|
145
|
-
required[:tabindex] = -1 if role == "dialog"
|
|
146
|
-
merged = lc_attributes(lc_attributes({ class: classes }, attributes), required)
|
|
147
|
-
render "lostcoords/ui/components/lc_floating_panel", id: id, label: label, title: title, controller: controller, role: role, trigger_variant: trigger_variant, attributes: merged, content: (capture(&block) if block)
|
|
148
|
-
end
|
|
149
61
|
end
|
|
150
62
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Lostcoords::Ui
|
|
2
|
+
module ToastHelper
|
|
3
|
+
# Render toasts inside lc_toast_region, the live region that announces them. A destructive toast
|
|
4
|
+
# is also an alert, so it interrupts.
|
|
5
|
+
def lc_toast(message = nil, tone: :success, timeout: 4000, **attributes, &block)
|
|
6
|
+
tone = lc_option!(:tone, tone, FeedbackHelper::ALERT_ICONS.keys)
|
|
7
|
+
raise ArgumentError, "timeout must be a nonnegative integer" unless timeout.is_a?(Integer) && timeout >= 0
|
|
8
|
+
|
|
9
|
+
defaults = { class: lc_glass_class(shape: :hud, extra: "flex items-center gap-related px-content py-heading text-label font-semibold transition-opacity data-leaving:opacity-0"), role: ("alert" if tone == :destructive), data: { slot: "toast", tone: tone, controller: "lc-toast", action: "click->lc-toast#dismiss", lc_toast_timeout_value: timeout } }
|
|
10
|
+
tag.div(**lc_attributes(defaults, attributes)) do
|
|
11
|
+
safe_join([ lc_alert_icon(tone: tone), tag.span(block ? capture(&block) : message) ])
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Fixed corner that holds toasts. Render it once in the layout and append toasts to it, from flash
|
|
16
|
+
# or a Turbo Stream; it shows in the top layer, above dialogs and sticky chrome.
|
|
17
|
+
def lc_toast_region(**attributes, &block)
|
|
18
|
+
defaults = { class: "lc-toast-region", role: "status", aria: { live: "polite" }, data: { slot: "toast-region", controller: "lc-toast-region" } }
|
|
19
|
+
tag.div(**lc_attributes(defaults, attributes)) { capture(&block) if block }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module Lostcoords::Ui
|
|
2
|
-
module
|
|
2
|
+
module TooltipHelper
|
|
3
3
|
TOOLTIP_SIDES = %i[top bottom].freeze
|
|
4
4
|
|
|
5
5
|
# Yields attributes for the trigger; splat them with `**tip` so the trigger is described by the tooltip.
|
|
@@ -9,8 +9,8 @@ module Lostcoords::Ui
|
|
|
9
9
|
lc_label!(id, :id)
|
|
10
10
|
side = lc_option!(:side, side, TOOLTIP_SIDES)
|
|
11
11
|
trigger = capture({ aria: { describedby: id } }, &block) if block
|
|
12
|
-
tag.span(**lc_attributes({ class: "lc-tooltip relative inline-flex", data: { slot: "tooltip", side: side, controller: "lc-tooltip", action: "
|
|
13
|
-
safe_join([ trigger, tag.span(text, id: id, role: "tooltip", class: "lc-tooltip__content", data: { slot: "tooltip-content" }) ].compact)
|
|
12
|
+
tag.span(**lc_attributes({ class: "lc-tooltip relative inline-flex", data: { slot: "tooltip", side: side, controller: "lc-tooltip", action: "mouseenter->lc-tooltip#show focusin->lc-tooltip#show mouseleave->lc-tooltip#hide focusout->lc-tooltip#hide" } }, attributes)) do
|
|
13
|
+
safe_join([ trigger, tag.span(text, id: id, role: "tooltip", class: "lc-tooltip__content", data: { slot: "tooltip-content", lc_tooltip_target: "content" }) ].compact)
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -3,6 +3,10 @@ module Lostcoords::Ui
|
|
|
3
3
|
HEADING_SIZES = { display: "text-display", title: "text-title", heading: "text-heading", subheading: "text-subheading" }.freeze
|
|
4
4
|
TEXT_SIZES = { body: "text-body", small: "text-small", label: "text-label", caption: "text-caption" }.freeze
|
|
5
5
|
TEXT_TONES = { default: "text-foreground", muted: "text-muted-foreground" }.freeze
|
|
6
|
+
KBD_BASE = "lc-kbd inline-flex items-center rounded-md bg-tile px-1.5 font-sans text-caption leading-5 whitespace-nowrap text-muted-foreground"
|
|
7
|
+
KBD_NAMES = { "mod" => "Ctrl", "ctrl" => "Ctrl", "alt" => "Alt", "shift" => "Shift", "meta" => "Meta", "escape" => "Esc", "enter" => "↵",
|
|
8
|
+
"arrowup" => "↑", "arrowdown" => "↓", "arrowleft" => "←", "arrowright" => "→" }.freeze
|
|
9
|
+
KBD_MODIFIERS = %w[mod ctrl alt shift meta].freeze
|
|
6
10
|
|
|
7
11
|
def lc_heading_class(size: :heading, trim: false, extra: nil)
|
|
8
12
|
size = lc_option!(:size, size, HEADING_SIZES.keys)
|
|
@@ -15,6 +19,10 @@ module Lostcoords::Ui
|
|
|
15
19
|
lc_tw("lc-text m-0", TEXT_SIZES.fetch(size), TEXT_TONES.fetch(tone), ("lc-trim" if trim), extra)
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
def lc_kbd_class(extra: nil)
|
|
23
|
+
lc_tw(KBD_BASE, extra)
|
|
24
|
+
end
|
|
25
|
+
|
|
18
26
|
def lc_heading(content = nil, level: 2, size: :heading, trim: false, **attributes, &block)
|
|
19
27
|
raise ArgumentError, "Invalid level: #{level.inspect}. Expected an integer from 1 to 6" unless (1..6).include?(level) && level.is_a?(Integer)
|
|
20
28
|
|
|
@@ -24,5 +32,27 @@ module Lostcoords::Ui
|
|
|
24
32
|
def lc_text(content = nil, size: :body, tone: :default, trim: false, **attributes, &block)
|
|
25
33
|
tag.p(content, **lc_attributes({ class: lc_text_class(size: size, tone: tone, trim: trim) }, attributes), &block)
|
|
26
34
|
end
|
|
35
|
+
|
|
36
|
+
# A key hint, such as lc_kbd("mod+k"). "mod" renders Ctrl and reads ⌘ on Apple platforms once JS runs.
|
|
37
|
+
def lc_kbd(keys, **attributes)
|
|
38
|
+
lc_label!(keys, :keys)
|
|
39
|
+
text = keys.split.map do |combo|
|
|
40
|
+
*modifiers, key = combo.downcase.split(/\+(?!\z)/)
|
|
41
|
+
names = modifiers.map { |modifier| KBD_MODIFIERS.include?(modifier) ? KBD_NAMES.fetch(modifier) : raise(ArgumentError, "Unknown modifier #{modifier.inspect} in #{keys.inspect}. Expected one of: #{KBD_MODIFIERS.join(', ')}") }
|
|
42
|
+
# A symbol such as ? already implies Shift; a letter or digit keeps it.
|
|
43
|
+
names.delete("Shift") if key.match?(/\A[^[:alnum:]]\z/)
|
|
44
|
+
[ *names, KBD_NAMES.fetch(key) { key.length == 1 ? key.upcase : key.capitalize } ].join(" ")
|
|
45
|
+
end.join(" / ")
|
|
46
|
+
tag.kbd(text, **lc_attributes({ class: lc_kbd_class, data: { controller: "lc-kbd", lc_kbd_keys_value: keys } }, attributes))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Data for the element a page-wide shortcut focuses (fields) or clicks (anything else):
|
|
50
|
+
# lc_button "Search", data: lc_shortcut_data("mod+k"). Shortcuts without mod, ctrl or alt
|
|
51
|
+
# don't fire while typing in a field; inside an open modal, only its own shortcuts do.
|
|
52
|
+
def lc_shortcut_data(keys, data = {})
|
|
53
|
+
lc_label!(keys, :keys)
|
|
54
|
+
data = data.to_h.symbolize_keys
|
|
55
|
+
data.merge(controller: [ data[:controller], "lc-shortcut" ].compact.join(" "), lc_shortcut_keys_value: keys)
|
|
56
|
+
end
|
|
27
57
|
end
|
|
28
58
|
end
|
|
@@ -3,7 +3,7 @@ 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.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
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"
|
|
2
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}"]
|
|
@@ -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
|
|
|
@@ -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
|
+
}
|