ruby_ui 1.2.0 → 1.3.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/lib/generators/ruby_ui/dependencies.yml +32 -10
- data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +24 -7
- data/lib/ruby_ui/avatar/avatar.rb +3 -0
- data/lib/ruby_ui/avatar/avatar_controller.js +33 -0
- data/lib/ruby_ui/avatar/avatar_fallback.rb +3 -0
- data/lib/ruby_ui/avatar/avatar_image.rb +4 -0
- data/lib/ruby_ui/base.rb +6 -0
- data/lib/ruby_ui/calendar/calendar.rb +3 -1
- data/lib/ruby_ui/calendar/calendar_controller.js +66 -7
- data/lib/ruby_ui/calendar/calendar_days.rb +20 -0
- data/lib/ruby_ui/calendar/calendar_docs.rb +9 -0
- data/lib/ruby_ui/combobox/combobox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_controller.js +56 -244
- data/lib/ruby_ui/combobox/combobox_item.rb +7 -5
- data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
- data/lib/ruby_ui/combobox/combobox_popover.rb +5 -0
- data/lib/ruby_ui/combobox/combobox_radio.rb +8 -1
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
- data/lib/ruby_ui/command/command_controller.js +10 -19
- data/lib/ruby_ui/command/command_dialog.rb +4 -1
- data/lib/ruby_ui/command/command_dialog_content.rb +2 -2
- data/lib/ruby_ui/command/command_dialog_controller.js +34 -0
- data/lib/ruby_ui/command/command_dialog_trigger.rb +2 -2
- data/lib/ruby_ui/date_picker/date_picker.rb +85 -0
- data/lib/ruby_ui/date_picker/date_picker_docs.rb +23 -0
- data/lib/ruby_ui/masked_input/masked_input.rb +1 -11
- data/lib/ruby_ui/masked_input/masked_input_controller.js +0 -13
- data/lib/ruby_ui/select/select_value.rb +2 -1
- data/lib/ruby_ui/sheet/sheet.rb +9 -1
- data/lib/ruby_ui/sheet/sheet_controller.js +6 -0
- data/lib/ruby_ui/theme_toggle/theme_toggle.rb +14 -2
- data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +27 -19
- data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +12 -42
- data/lib/ruby_ui/toast/toast.rb +18 -0
- data/lib/ruby_ui/toast/toast_action.rb +27 -0
- data/lib/ruby_ui/toast/toast_cancel.rb +27 -0
- data/lib/ruby_ui/toast/toast_close.rb +40 -0
- data/lib/ruby_ui/toast/toast_controller.js +151 -0
- data/lib/ruby_ui/toast/toast_description.rb +18 -0
- data/lib/ruby_ui/toast/toast_docs.rb +12 -0
- data/lib/ruby_ui/toast/toast_icon.rb +65 -0
- data/lib/ruby_ui/toast/toast_item.rb +72 -0
- data/lib/ruby_ui/toast/toast_region.rb +124 -0
- data/lib/ruby_ui/toast/toast_title.rb +18 -0
- data/lib/ruby_ui/toast/toaster_controller.js +306 -0
- data/lib/ruby_ui/toggle/toggle.rb +101 -0
- data/lib/ruby_ui/toggle/toggle_controller.js +33 -0
- data/lib/ruby_ui/toggle_group/toggle_group.rb +119 -0
- data/lib/ruby_ui/toggle_group/toggle_group_controller.js +126 -0
- data/lib/ruby_ui/toggle_group/toggle_group_item.rb +67 -0
- data/lib/ruby_ui/tooltip/tooltip_content.rb +12 -5
- data/lib/ruby_ui/tooltip/tooltip_controller.js +58 -22
- data/lib/ruby_ui/tooltip/tooltip_docs.rb +13 -0
- data/lib/ruby_ui/tooltip/tooltip_trigger.rb +10 -3
- data/lib/ruby_ui.rb +3 -1
- metadata +30 -14
- data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +0 -16
- data/lib/ruby_ui/theme_toggle/set_light_mode.rb +0 -16
|
@@ -8,7 +8,7 @@ module RubyUI
|
|
|
8
8
|
].freeze
|
|
9
9
|
|
|
10
10
|
def initialize(keybindings: DEFAULT_KEYBINDINGS, **attrs)
|
|
11
|
-
@keybindings = keybindings.map { |kb| "#{kb}->ruby-ui--command#open" }
|
|
11
|
+
@keybindings = keybindings.map { |kb| "#{kb}->ruby-ui--command-dialog#open" }
|
|
12
12
|
super(**attrs)
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -21,7 +21,7 @@ module RubyUI
|
|
|
21
21
|
def default_attrs
|
|
22
22
|
{
|
|
23
23
|
data: {
|
|
24
|
-
action: ["click->ruby-ui--command#open", @keybindings.join(" ")]
|
|
24
|
+
action: ["click->ruby-ui--command-dialog#open", @keybindings.join(" ")]
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module RubyUI
|
|
6
|
+
class DatePicker < Base
|
|
7
|
+
def initialize(
|
|
8
|
+
id: nil,
|
|
9
|
+
name: nil,
|
|
10
|
+
label: "Select a date",
|
|
11
|
+
value: nil,
|
|
12
|
+
placeholder: "Select a date",
|
|
13
|
+
selected_date: value,
|
|
14
|
+
date_format: "yyyy-MM-dd",
|
|
15
|
+
popover_options: {},
|
|
16
|
+
input_attrs: {},
|
|
17
|
+
calendar_attrs: {},
|
|
18
|
+
trigger_attrs: {},
|
|
19
|
+
content_attrs: {},
|
|
20
|
+
**attrs
|
|
21
|
+
)
|
|
22
|
+
@id = id || "date-picker-#{SecureRandom.hex(4)}"
|
|
23
|
+
@name = name
|
|
24
|
+
@label = label
|
|
25
|
+
@value = value || selected_date&.to_s
|
|
26
|
+
@placeholder = placeholder
|
|
27
|
+
@selected_date = selected_date
|
|
28
|
+
@date_format = date_format
|
|
29
|
+
@popover_options = {trigger: "click"}.merge(popover_options)
|
|
30
|
+
@input_attrs = input_attrs
|
|
31
|
+
@calendar_attrs = calendar_attrs
|
|
32
|
+
@trigger_attrs = trigger_attrs
|
|
33
|
+
@content_attrs = content_attrs
|
|
34
|
+
super(**attrs)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def view_template
|
|
38
|
+
div(**attrs) do
|
|
39
|
+
RubyUI.Popover(options: @popover_options) do
|
|
40
|
+
RubyUI.PopoverTrigger(**trigger_attrs) do
|
|
41
|
+
div(class: "grid w-full max-w-sm items-center gap-1.5") do
|
|
42
|
+
label(for: @id) { @label } if @label
|
|
43
|
+
RubyUI.Input(**input_attrs)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
RubyUI.PopoverContent(**content_attrs) do
|
|
47
|
+
RubyUI.Calendar(input_id: "##{@id}", selected_date: @selected_date, date_format: @date_format, **calendar_attrs)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def default_attrs
|
|
56
|
+
{
|
|
57
|
+
class: "space-y-4 w-[260px]"
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def trigger_attrs
|
|
62
|
+
mix({class: "w-full"}, @trigger_attrs)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def input_attrs
|
|
66
|
+
mix({
|
|
67
|
+
type: "string",
|
|
68
|
+
placeholder: @placeholder,
|
|
69
|
+
id: @id,
|
|
70
|
+
name: @name,
|
|
71
|
+
value: @value,
|
|
72
|
+
data_controller: "ruby-ui--calendar-input",
|
|
73
|
+
class: "rounded-md border shadow"
|
|
74
|
+
}.compact, @input_attrs)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def calendar_attrs
|
|
78
|
+
mix({}, @calendar_attrs)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def content_attrs
|
|
82
|
+
mix({}, @content_attrs)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Views::Docs::DatePicker < Views::Base
|
|
4
|
+
def view_template
|
|
5
|
+
component = "DatePicker"
|
|
6
|
+
|
|
7
|
+
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
|
|
8
|
+
render Docs::Header.new(title: "Date Picker", description: "A date picker component with input.")
|
|
9
|
+
|
|
10
|
+
Heading(level: 2) { "Usage" }
|
|
11
|
+
|
|
12
|
+
render Docs::VisualCodeExample.new(title: "Single Date", context: self) do
|
|
13
|
+
<<~RUBY
|
|
14
|
+
DatePicker(id: "date")
|
|
15
|
+
RUBY
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
render Components::ComponentSetup::Tabs.new(component_name: component)
|
|
19
|
+
|
|
20
|
+
render Docs::ComponentsTable.new(component_files(component))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -2,18 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class MaskedInput < Base
|
|
5
|
-
def initialize(save_unmasked: false, **attrs)
|
|
6
|
-
@save_unmasked = save_unmasked
|
|
7
|
-
super(**attrs)
|
|
8
|
-
end
|
|
9
|
-
|
|
10
5
|
def view_template
|
|
11
|
-
|
|
12
|
-
Input(type: "text", **attrs.merge(name: "#{attrs[:name]}-masked"))
|
|
13
|
-
input(type: "hidden", name: attrs[:name], value: attrs[:value])
|
|
14
|
-
else
|
|
15
|
-
Input(type: "text", **attrs)
|
|
16
|
-
end
|
|
6
|
+
Input(type: "text", **attrs)
|
|
17
7
|
end
|
|
18
8
|
|
|
19
9
|
private
|
|
@@ -5,18 +5,5 @@ import { MaskInput } from "maska";
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
connect() {
|
|
7
7
|
new MaskInput(this.element)
|
|
8
|
-
this.#boundSync = this.#sync.bind(this);
|
|
9
|
-
this.element.addEventListener("maska", this.#boundSync);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
disconnect() {
|
|
13
|
-
this.element.removeEventListener("maska", this.#boundSync);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
#boundSync = null;
|
|
17
|
-
|
|
18
|
-
#sync(event) {
|
|
19
|
-
const hidden = this.element.nextElementSibling;
|
|
20
|
-
if (hidden?.type === "hidden") hidden.value = event.detail.unmasked;
|
|
21
8
|
}
|
|
22
9
|
}
|
data/lib/ruby_ui/sheet/sheet.rb
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class Sheet < Base
|
|
5
|
+
def initialize(open: false, **attrs)
|
|
6
|
+
@open = open
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
5
10
|
def view_template(&)
|
|
6
11
|
div(**attrs, &)
|
|
7
12
|
end
|
|
@@ -10,7 +15,10 @@ module RubyUI
|
|
|
10
15
|
|
|
11
16
|
def default_attrs
|
|
12
17
|
{
|
|
13
|
-
data: {
|
|
18
|
+
data: {
|
|
19
|
+
controller: "ruby-ui--sheet",
|
|
20
|
+
ruby_ui__sheet_open_value: @open.to_s
|
|
21
|
+
}
|
|
14
22
|
}
|
|
15
23
|
end
|
|
16
24
|
end
|
|
@@ -3,6 +3,12 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
3
3
|
export default class extends Controller {
|
|
4
4
|
static targets = ["content"]
|
|
5
5
|
|
|
6
|
+
static values = { open: false }
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
if (this.openValue) this.open()
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
open() {
|
|
7
13
|
document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML)
|
|
8
14
|
}
|
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class ThemeToggle < Base
|
|
5
|
-
def view_template(&)
|
|
6
|
-
|
|
5
|
+
def view_template(&block)
|
|
6
|
+
RubyUI.Toggle(
|
|
7
|
+
variant: :default,
|
|
8
|
+
size: :default,
|
|
9
|
+
aria: {label: "Toggle theme"},
|
|
10
|
+
wrapper: {
|
|
11
|
+
data: {
|
|
12
|
+
controller: "ruby-ui--theme-toggle",
|
|
13
|
+
action: "ruby-ui--toggle:change->ruby-ui--theme-toggle#apply"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
**attrs,
|
|
17
|
+
&block
|
|
18
|
+
)
|
|
7
19
|
end
|
|
8
20
|
end
|
|
9
21
|
end
|
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
|
+
// Connects to data-controller="ruby-ui--theme-toggle"
|
|
4
|
+
// Sits on the same wrapper as ruby-ui--toggle. Listens for the toggle's
|
|
5
|
+
// ruby-ui--toggle:change event. pressed = dark mode.
|
|
3
6
|
export default class extends Controller {
|
|
4
|
-
|
|
5
|
-
this.
|
|
7
|
+
connect() {
|
|
8
|
+
this.applyTheme(this.currentTheme())
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} else {
|
|
14
|
-
document.documentElement.classList.remove('dark')
|
|
15
|
-
document.documentElement.classList.add('light')
|
|
16
|
-
}
|
|
11
|
+
apply(event) {
|
|
12
|
+
const pressed = event.detail?.pressed
|
|
13
|
+
const theme = pressed ? "dark" : "light"
|
|
14
|
+
localStorage.theme = theme
|
|
15
|
+
this.applyTheme(theme)
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
localStorage.theme
|
|
22
|
-
|
|
18
|
+
currentTheme() {
|
|
19
|
+
if (localStorage.theme === "dark") return "dark"
|
|
20
|
+
if (localStorage.theme === "light") return "light"
|
|
21
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
applyTheme(theme) {
|
|
25
|
+
const html = document.documentElement
|
|
26
|
+
if (theme === "dark") {
|
|
27
|
+
html.classList.add("dark")
|
|
28
|
+
html.classList.remove("light")
|
|
29
|
+
} else {
|
|
30
|
+
html.classList.add("light")
|
|
31
|
+
html.classList.remove("dark")
|
|
32
|
+
}
|
|
33
|
+
// Flip the sibling Toggle controller's pressed value; it will propagate
|
|
34
|
+
// aria-pressed / data-state to the button target.
|
|
35
|
+
const dark = theme === "dark"
|
|
36
|
+
this.element.setAttribute("data-ruby-ui--toggle-pressed-value", dark ? "true" : "false")
|
|
29
37
|
}
|
|
30
38
|
}
|
|
@@ -11,39 +11,17 @@ class Views::Docs::ThemeToggle < Views::Base
|
|
|
11
11
|
|
|
12
12
|
render Docs::VisualCodeExample.new(title: "With icon", context: self) do
|
|
13
13
|
<<~RUBY
|
|
14
|
-
ThemeToggle do
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
SetDarkMode do
|
|
32
|
-
Button(variant: :ghost, icon: true) do
|
|
33
|
-
svg(
|
|
34
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
35
|
-
viewbox: "0 0 24 24",
|
|
36
|
-
fill: "currentColor",
|
|
37
|
-
class: "w-4 h-4"
|
|
38
|
-
) do |s|
|
|
39
|
-
s.path(
|
|
40
|
-
fill_rule: "evenodd",
|
|
41
|
-
d:
|
|
42
|
-
"M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z",
|
|
43
|
-
clip_rule: "evenodd"
|
|
44
|
-
)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
14
|
+
ThemeToggle do
|
|
15
|
+
svg(
|
|
16
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
17
|
+
viewbox: "0 0 24 24",
|
|
18
|
+
fill: "currentColor",
|
|
19
|
+
class: "w-4 h-4"
|
|
20
|
+
) do |s|
|
|
21
|
+
s.path(
|
|
22
|
+
d:
|
|
23
|
+
"M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"
|
|
24
|
+
)
|
|
47
25
|
end
|
|
48
26
|
end
|
|
49
27
|
RUBY
|
|
@@ -51,15 +29,7 @@ class Views::Docs::ThemeToggle < Views::Base
|
|
|
51
29
|
|
|
52
30
|
render Docs::VisualCodeExample.new(title: "With text", context: self) do
|
|
53
31
|
<<~RUBY
|
|
54
|
-
ThemeToggle
|
|
55
|
-
SetLightMode do
|
|
56
|
-
Button(variant: :primary) { "Light" }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
SetDarkMode do
|
|
60
|
-
Button(variant: :primary) { "Dark" }
|
|
61
|
-
end
|
|
62
|
-
end
|
|
32
|
+
ThemeToggle { "Toggle Theme" }
|
|
63
33
|
RUBY
|
|
64
34
|
end
|
|
65
35
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
module Toast
|
|
5
|
+
FLASH_VARIANTS = {
|
|
6
|
+
"notice" => :info,
|
|
7
|
+
"alert" => :warning,
|
|
8
|
+
"success" => :success,
|
|
9
|
+
"error" => :error,
|
|
10
|
+
"warning" => :warning,
|
|
11
|
+
"info" => :info
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def self.flash_variant(key)
|
|
15
|
+
FLASH_VARIANTS[key.to_s] || :default
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastAction < Base
|
|
5
|
+
def initialize(label:, on: nil, **attrs)
|
|
6
|
+
@label = label
|
|
7
|
+
@on = on
|
|
8
|
+
super(**attrs)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def view_template
|
|
12
|
+
button(**attrs) { @label }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def default_attrs
|
|
18
|
+
data = {slot: "action"}
|
|
19
|
+
data[:action] = @on if @on
|
|
20
|
+
{
|
|
21
|
+
type: "button",
|
|
22
|
+
data: data,
|
|
23
|
+
class: "inline-flex h-6 shrink-0 cursor-pointer items-center justify-center rounded px-2 text-xs font-medium bg-foreground text-background border-0 ml-auto hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring transition-opacity disabled:pointer-events-none disabled:opacity-50"
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastCancel < Base
|
|
5
|
+
def initialize(label:, **attrs)
|
|
6
|
+
@label = label
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def view_template
|
|
11
|
+
button(**attrs) { @label }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def default_attrs
|
|
17
|
+
{
|
|
18
|
+
type: "button",
|
|
19
|
+
data: {
|
|
20
|
+
slot: "cancel",
|
|
21
|
+
action: "click->ruby-ui--toast#dismiss"
|
|
22
|
+
},
|
|
23
|
+
class: "inline-flex h-6 shrink-0 cursor-pointer items-center justify-center rounded px-2 text-xs font-medium bg-foreground/10 text-foreground border-0 ml-auto hover:bg-foreground/15 focus:outline-none focus:ring-2 focus:ring-ring transition-colors"
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastClose < Base
|
|
5
|
+
def view_template
|
|
6
|
+
button(**attrs) do
|
|
7
|
+
svg(
|
|
8
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
9
|
+
width: "14",
|
|
10
|
+
height: "14",
|
|
11
|
+
viewbox: "0 0 24 24",
|
|
12
|
+
fill: "none",
|
|
13
|
+
stroke: "currentColor",
|
|
14
|
+
stroke_width: "2",
|
|
15
|
+
stroke_linecap: "round",
|
|
16
|
+
stroke_linejoin: "round",
|
|
17
|
+
class: "size-3.5"
|
|
18
|
+
) do |s|
|
|
19
|
+
s.path(d: "M18 6 6 18")
|
|
20
|
+
s.path(d: "m6 6 12 12")
|
|
21
|
+
end
|
|
22
|
+
span(class: "sr-only") { "Close" }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def default_attrs
|
|
29
|
+
{
|
|
30
|
+
type: "button",
|
|
31
|
+
aria_label: "Close toast",
|
|
32
|
+
data: {
|
|
33
|
+
slot: "close",
|
|
34
|
+
action: "click->ruby-ui--toast#dismiss"
|
|
35
|
+
},
|
|
36
|
+
class: "absolute right-2 top-2 size-6 cursor-pointer rounded-md text-foreground/60 p-0 flex items-center justify-center transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
const SWIPE_THRESHOLD = 45
|
|
4
|
+
const TIME_BEFORE_UNMOUNT = 200
|
|
5
|
+
|
|
6
|
+
// Connects to data-controller="ruby-ui--toast"
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static values = {
|
|
9
|
+
duration: { type: Number, default: 4000 },
|
|
10
|
+
dismissible: { type: Boolean, default: true },
|
|
11
|
+
invert: { type: Boolean, default: false },
|
|
12
|
+
onDismiss: String,
|
|
13
|
+
onAutoClose: String,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
connect() {
|
|
17
|
+
this._timer = null
|
|
18
|
+
this._startedAt = 0
|
|
19
|
+
this._remaining = this.durationValue
|
|
20
|
+
this._paused = false
|
|
21
|
+
this._swipe = { active: false, x: 0, y: 0, startedAt: 0 }
|
|
22
|
+
|
|
23
|
+
this._onPointerDown = this._onPointerDown.bind(this)
|
|
24
|
+
this._onPointerMove = this._onPointerMove.bind(this)
|
|
25
|
+
this._onPointerUp = this._onPointerUp.bind(this)
|
|
26
|
+
this._onPointerEnter = () => this._pause()
|
|
27
|
+
this._onPointerLeave = () => { if (!this._swipe.active) this._resume() }
|
|
28
|
+
this._onKeyDown = this._onKeyDown.bind(this)
|
|
29
|
+
this._onForceDismiss = (e) => { e.stopPropagation(); this._close() }
|
|
30
|
+
this._onRestart = () => this._restart()
|
|
31
|
+
this._onRegionPause = () => this._pause()
|
|
32
|
+
this._onRegionResume = () => this._resume()
|
|
33
|
+
|
|
34
|
+
this.element.addEventListener("pointerdown", this._onPointerDown)
|
|
35
|
+
this.element.addEventListener("pointerenter", this._onPointerEnter)
|
|
36
|
+
this.element.addEventListener("pointerleave", this._onPointerLeave)
|
|
37
|
+
this.element.addEventListener("keydown", this._onKeyDown)
|
|
38
|
+
this.element.addEventListener("ruby-ui:toast:force-dismiss", this._onForceDismiss)
|
|
39
|
+
this.element.addEventListener("ruby-ui:toast:restart", this._onRestart)
|
|
40
|
+
document.addEventListener("ruby-ui:toast:pause", this._onRegionPause)
|
|
41
|
+
document.addEventListener("ruby-ui:toast:resume", this._onRegionResume)
|
|
42
|
+
|
|
43
|
+
requestAnimationFrame(() => {
|
|
44
|
+
this.element.dataset.state = "open"
|
|
45
|
+
this._start()
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
disconnect() {
|
|
50
|
+
this._clearTimer()
|
|
51
|
+
this.element.removeEventListener("pointerdown", this._onPointerDown)
|
|
52
|
+
this.element.removeEventListener("pointerenter", this._onPointerEnter)
|
|
53
|
+
this.element.removeEventListener("pointerleave", this._onPointerLeave)
|
|
54
|
+
this.element.removeEventListener("keydown", this._onKeyDown)
|
|
55
|
+
this.element.removeEventListener("ruby-ui:toast:force-dismiss", this._onForceDismiss)
|
|
56
|
+
this.element.removeEventListener("ruby-ui:toast:restart", this._onRestart)
|
|
57
|
+
document.removeEventListener("ruby-ui:toast:pause", this._onRegionPause)
|
|
58
|
+
document.removeEventListener("ruby-ui:toast:resume", this._onRegionResume)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
dismiss(e) {
|
|
62
|
+
e?.preventDefault()
|
|
63
|
+
if (!this.dismissibleValue) return
|
|
64
|
+
this._close("dismiss")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_close(reason) {
|
|
68
|
+
if (this.element.dataset.state === "closing") return
|
|
69
|
+
this.element.dataset.state = "closing"
|
|
70
|
+
this.element.dispatchEvent(new CustomEvent(reason === "auto" ? "ruby-ui:toast:auto-close" : "ruby-ui:toast:dismiss", { bubbles: true, detail: { id: this.element.id } }))
|
|
71
|
+
setTimeout(() => this.element.remove(), TIME_BEFORE_UNMOUNT)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_start() {
|
|
75
|
+
if (!Number.isFinite(this.durationValue) || this.durationValue <= 0) return
|
|
76
|
+
this._startedAt = performance.now()
|
|
77
|
+
this._remaining = this.durationValue
|
|
78
|
+
this._timer = setTimeout(() => this._close("auto"), this._remaining)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_restart() {
|
|
82
|
+
this._clearTimer()
|
|
83
|
+
this._start()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_pause() {
|
|
87
|
+
if (this._paused || !this._timer) return
|
|
88
|
+
this._paused = true
|
|
89
|
+
clearTimeout(this._timer)
|
|
90
|
+
this._timer = null
|
|
91
|
+
this._remaining -= performance.now() - this._startedAt
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_resume() {
|
|
95
|
+
if (!this._paused) return
|
|
96
|
+
this._paused = false
|
|
97
|
+
if (this._remaining <= 0) return this._close("auto")
|
|
98
|
+
this._startedAt = performance.now()
|
|
99
|
+
this._timer = setTimeout(() => this._close("auto"), this._remaining)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_clearTimer() {
|
|
103
|
+
if (this._timer) clearTimeout(this._timer)
|
|
104
|
+
this._timer = null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_onKeyDown(e) {
|
|
108
|
+
if (e.key === "Escape" && this.dismissibleValue) this.dismiss(e)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_onPointerDown(e) {
|
|
112
|
+
if (!this.dismissibleValue) return
|
|
113
|
+
if (e.target.closest("button")) return
|
|
114
|
+
try { this.element.setPointerCapture(e.pointerId) } catch {}
|
|
115
|
+
this._swipe = { active: true, x: e.clientX, y: e.clientY, startedAt: performance.now(), pointerId: e.pointerId }
|
|
116
|
+
this.element.dataset.swipe = "start"
|
|
117
|
+
this.element.addEventListener("pointermove", this._onPointerMove)
|
|
118
|
+
this.element.addEventListener("pointerup", this._onPointerUp)
|
|
119
|
+
this.element.addEventListener("pointercancel", this._onPointerUp)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_onPointerMove(e) {
|
|
123
|
+
const dx = e.clientX - this._swipe.x
|
|
124
|
+
const dy = e.clientY - this._swipe.y
|
|
125
|
+
this.element.dataset.swipe = "move"
|
|
126
|
+
this.element.style.transform = `translate(${dx}px, ${dy}px)`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_onPointerUp(e) {
|
|
130
|
+
const dx = e.clientX - this._swipe.x
|
|
131
|
+
const dy = e.clientY - this._swipe.y
|
|
132
|
+
const dist = Math.hypot(dx, dy)
|
|
133
|
+
const dt = performance.now() - this._swipe.startedAt
|
|
134
|
+
const velocity = dist / Math.max(dt, 1)
|
|
135
|
+
this.element.removeEventListener("pointermove", this._onPointerMove)
|
|
136
|
+
this.element.removeEventListener("pointerup", this._onPointerUp)
|
|
137
|
+
this.element.removeEventListener("pointercancel", this._onPointerUp)
|
|
138
|
+
this._swipe.active = false
|
|
139
|
+
if (dist > SWIPE_THRESHOLD || velocity > 0.5) {
|
|
140
|
+
this.element.style.setProperty("--swipe-end-x", `${Math.sign(dx) * 500}px`)
|
|
141
|
+
this.element.style.setProperty("--swipe-end-y", `${Math.sign(dy) * 500}px`)
|
|
142
|
+
this.element.dataset.swipe = "end"
|
|
143
|
+
this.element.style.transform = ""
|
|
144
|
+
this._close("dismiss")
|
|
145
|
+
} else {
|
|
146
|
+
this.element.dataset.swipe = "cancel"
|
|
147
|
+
this.element.style.transform = ""
|
|
148
|
+
this._resume()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastDescription < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
data: {slot: "description"},
|
|
14
|
+
class: "font-normal leading-[1.4] text-muted-foreground"
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastDocs < Phlex::HTML
|
|
5
|
+
def view_template
|
|
6
|
+
div(class: "space-y-4") do
|
|
7
|
+
h2 { "Toast" }
|
|
8
|
+
p { "Hotwire-native sonner port. Mount once; trigger via Turbo Stream or window.RubyUI.toast." }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|