ruby_ui 1.1.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/component_generator.rb +5 -1
- data/lib/generators/ruby_ui/dependencies.yml +32 -0
- data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
- data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +27 -6
- 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_badge.rb +17 -0
- data/lib/ruby_ui/combobox/combobox_badge_trigger.rb +47 -0
- data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
- data/lib/ruby_ui/combobox/combobox_controller.js +4 -2
- data/lib/ruby_ui/combobox/combobox_docs.rb +199 -64
- data/lib/ruby_ui/combobox/combobox_input_trigger.rb +64 -0
- data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
- 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/data_table/data_table.rb +29 -0
- data/lib/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
- data/lib/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
- data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +14 -0
- data/lib/ruby_ui/data_table/data_table_controller.js +57 -0
- data/lib/ruby_ui/data_table/data_table_docs.rb +180 -0
- data/lib/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
- data/lib/ruby_ui/data_table/data_table_form.rb +39 -0
- data/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_pagination.rb +100 -0
- data/lib/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
- data/lib/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
- data/lib/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
- data/lib/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
- data/lib/ruby_ui/data_table/data_table_search.rb +57 -0
- data/lib/ruby_ui/data_table/data_table_search_controller.js +62 -0
- data/lib/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
- data/lib/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
- data/lib/ruby_ui/data_table/data_table_sort_head.rb +112 -0
- data/lib/ruby_ui/data_table/data_table_toolbar.rb +15 -0
- 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/native_select/native_select.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_docs.rb +83 -0
- data/lib/ruby_ui/native_select/native_select_group.rb +15 -0
- data/lib/ruby_ui/native_select/native_select_icon.rb +39 -0
- data/lib/ruby_ui/native_select/native_select_option.rb +15 -0
- 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 +66 -10
- data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +0 -16
- data/lib/ruby_ui/theme_toggle/set_light_mode.rb +0 -16
|
@@ -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
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastIcon < Base
|
|
5
|
+
def initialize(variant: nil, **attrs)
|
|
6
|
+
@variant = variant&.to_sym
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def view_template
|
|
11
|
+
return unless renderable?
|
|
12
|
+
span(**attrs) do
|
|
13
|
+
svg(
|
|
14
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
15
|
+
width: "16",
|
|
16
|
+
height: "16",
|
|
17
|
+
viewbox: "0 0 24 24",
|
|
18
|
+
fill: "none",
|
|
19
|
+
stroke: "currentColor",
|
|
20
|
+
stroke_width: "2",
|
|
21
|
+
stroke_linecap: "round",
|
|
22
|
+
stroke_linejoin: "round",
|
|
23
|
+
class: "#{svg_classes} -ml-px"
|
|
24
|
+
) { |s| paths(s) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def renderable?
|
|
31
|
+
%i[success error warning info loading].include?(@variant)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def svg_classes
|
|
35
|
+
base = "size-4"
|
|
36
|
+
(@variant == :loading) ? "#{base} animate-spin" : base
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def paths(s)
|
|
40
|
+
case @variant
|
|
41
|
+
when :success
|
|
42
|
+
s.circle(cx: "12", cy: "12", r: "10")
|
|
43
|
+
s.path(d: "m9 12 2 2 4-4")
|
|
44
|
+
when :error
|
|
45
|
+
s.path(d: "M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z")
|
|
46
|
+
s.path(d: "m15 9-6 6")
|
|
47
|
+
s.path(d: "m9 9 6 6")
|
|
48
|
+
when :warning
|
|
49
|
+
s.path(d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3")
|
|
50
|
+
s.path(d: "M12 9v4")
|
|
51
|
+
s.path(d: "M12 17h.01")
|
|
52
|
+
when :info
|
|
53
|
+
s.circle(cx: "12", cy: "12", r: "10")
|
|
54
|
+
s.path(d: "M12 16v-4")
|
|
55
|
+
s.path(d: "M12 8h.01")
|
|
56
|
+
when :loading
|
|
57
|
+
s.path(d: "M21 12a9 9 0 1 1-6.219-8.56")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def default_attrs
|
|
62
|
+
{data: {slot: "icon"}, class: "shrink-0 inline-flex items-center justify-start relative size-4 -ml-[3px] mr-1 text-foreground"}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastItem < Base
|
|
5
|
+
ALERT_VARIANTS = %i[error].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(
|
|
8
|
+
variant: :default,
|
|
9
|
+
id: nil,
|
|
10
|
+
duration: nil,
|
|
11
|
+
dismissible: true,
|
|
12
|
+
invert: false,
|
|
13
|
+
on_dismiss: nil,
|
|
14
|
+
on_auto_close: nil,
|
|
15
|
+
**attrs
|
|
16
|
+
)
|
|
17
|
+
@variant = variant.to_sym
|
|
18
|
+
@id = id
|
|
19
|
+
@duration = duration
|
|
20
|
+
@dismissible = dismissible
|
|
21
|
+
@invert = invert
|
|
22
|
+
@on_dismiss = on_dismiss
|
|
23
|
+
@on_auto_close = on_auto_close
|
|
24
|
+
super(**attrs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def view_template(&)
|
|
28
|
+
li(**attrs, &)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def default_attrs
|
|
34
|
+
a = {
|
|
35
|
+
role: ALERT_VARIANTS.include?(@variant) ? "alert" : "status",
|
|
36
|
+
aria_atomic: "true",
|
|
37
|
+
tabindex: "0",
|
|
38
|
+
data: {
|
|
39
|
+
variant: @variant.to_s,
|
|
40
|
+
state: "pending",
|
|
41
|
+
swipe: "none",
|
|
42
|
+
controller: "ruby-ui--toast",
|
|
43
|
+
ruby_ui__toaster_target: "toast",
|
|
44
|
+
ruby_ui__toast_dismissible_value: @dismissible.to_s,
|
|
45
|
+
ruby_ui__toast_invert_value: @invert.to_s
|
|
46
|
+
},
|
|
47
|
+
class: item_classes
|
|
48
|
+
}
|
|
49
|
+
a[:id] = @id if @id
|
|
50
|
+
a[:data][:ruby_ui__toast_duration_value] = @duration.to_s if @duration
|
|
51
|
+
a[:data][:ruby_ui__toast_on_dismiss_value] = @on_dismiss if @on_dismiss
|
|
52
|
+
a[:data][:ruby_ui__toast_on_auto_close_value] = @on_auto_close if @on_auto_close
|
|
53
|
+
a
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def item_classes
|
|
57
|
+
<<~CLASSES.tr("\n", " ").squeeze(" ").strip
|
|
58
|
+
group/toast pointer-events-auto absolute left-0 right-0
|
|
59
|
+
flex w-[356px] max-w-full items-center gap-1.5
|
|
60
|
+
rounded-lg border bg-popover text-popover-foreground
|
|
61
|
+
border-border p-4 text-[13px] shadow-[0_4px_12px_rgba(0,0,0,0.1)]
|
|
62
|
+
group-data-[close-button=true]/toaster:pr-10
|
|
63
|
+
transition-[transform,opacity] duration-300 ease-out
|
|
64
|
+
will-change-transform
|
|
65
|
+
opacity-[var(--opacity,1)]
|
|
66
|
+
data-[state=pending]:opacity-0
|
|
67
|
+
data-[state=closing]:opacity-0
|
|
68
|
+
data-[swipe=move]:transition-none
|
|
69
|
+
CLASSES
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastRegion < Base
|
|
5
|
+
SKELETON_VARIANTS = %i[default success error warning info loading].freeze
|
|
6
|
+
|
|
7
|
+
def initialize(
|
|
8
|
+
position: :bottom_right,
|
|
9
|
+
expand: false,
|
|
10
|
+
max: 3,
|
|
11
|
+
duration: 4000,
|
|
12
|
+
gap: 14,
|
|
13
|
+
offset: 24,
|
|
14
|
+
theme: :system,
|
|
15
|
+
rich_colors: false,
|
|
16
|
+
close_button: false,
|
|
17
|
+
hotkey: %w[alt t],
|
|
18
|
+
dir: :ltr,
|
|
19
|
+
flash: nil,
|
|
20
|
+
**attrs
|
|
21
|
+
)
|
|
22
|
+
@position = position.to_sym
|
|
23
|
+
@expand = expand
|
|
24
|
+
@max = max
|
|
25
|
+
@duration = duration
|
|
26
|
+
@gap = gap
|
|
27
|
+
@offset = offset
|
|
28
|
+
@theme = theme.to_sym
|
|
29
|
+
@rich_colors = rich_colors
|
|
30
|
+
@close_button = close_button
|
|
31
|
+
@hotkey = hotkey
|
|
32
|
+
@dir = dir
|
|
33
|
+
@flash = flash
|
|
34
|
+
super(**attrs)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def view_template(&block)
|
|
38
|
+
div(**attrs) do
|
|
39
|
+
ol(id: "ruby-ui-toaster", class: "pointer-events-auto relative m-0 p-0 list-none w-[356px] max-w-full") do
|
|
40
|
+
render_flash if @flash
|
|
41
|
+
yield(self) if block
|
|
42
|
+
end
|
|
43
|
+
SKELETON_VARIANTS.each { |v| skeleton(v) }
|
|
44
|
+
slot_template("actionTpl") { render RubyUI::ToastAction.new(label: "") }
|
|
45
|
+
slot_template("cancelTpl") { render RubyUI::ToastCancel.new(label: "") }
|
|
46
|
+
slot_template("closeTpl") { render RubyUI::ToastClose.new }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def render_flash
|
|
53
|
+
@flash.each do |key, message|
|
|
54
|
+
next if message.nil? || message.to_s.empty?
|
|
55
|
+
variant = RubyUI::Toast.flash_variant(key)
|
|
56
|
+
render RubyUI::ToastItem.new(variant: variant, id: "flash-#{key}") do
|
|
57
|
+
render RubyUI::ToastIcon.new(variant: variant)
|
|
58
|
+
render RubyUI::ToastTitle.new { message.to_s }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def skeleton(variant)
|
|
64
|
+
template(
|
|
65
|
+
data: {
|
|
66
|
+
ruby_ui__toaster_target: "skeleton",
|
|
67
|
+
variant: variant.to_s
|
|
68
|
+
}
|
|
69
|
+
) do
|
|
70
|
+
render RubyUI::ToastItem.new(variant: variant) do
|
|
71
|
+
render RubyUI::ToastIcon.new(variant: variant)
|
|
72
|
+
div(class: "flex flex-col gap-0.5 flex-1 min-w-0") do
|
|
73
|
+
render RubyUI::ToastTitle.new
|
|
74
|
+
render RubyUI::ToastDescription.new
|
|
75
|
+
end
|
|
76
|
+
render RubyUI::ToastClose.new if @close_button
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def slot_template(target_name, &)
|
|
82
|
+
template(data: {ruby_ui__toaster_target: target_name}, &)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def default_attrs
|
|
86
|
+
{
|
|
87
|
+
id: "ruby-ui-toaster-region",
|
|
88
|
+
role: "region",
|
|
89
|
+
aria_label: "Notifications",
|
|
90
|
+
aria_live: "polite",
|
|
91
|
+
data: {
|
|
92
|
+
controller: "ruby-ui--toaster",
|
|
93
|
+
turbo_permanent: "",
|
|
94
|
+
close_button: @close_button.to_s,
|
|
95
|
+
position: @position.to_s.tr("_", "-"),
|
|
96
|
+
ruby_ui__toaster_position_value: @position.to_s.tr("_", "-"),
|
|
97
|
+
ruby_ui__toaster_expand_value: @expand.to_s,
|
|
98
|
+
ruby_ui__toaster_max_value: @max.to_s,
|
|
99
|
+
ruby_ui__toaster_duration_value: @duration.to_s,
|
|
100
|
+
ruby_ui__toaster_gap_value: @gap.to_s,
|
|
101
|
+
ruby_ui__toaster_offset_value: @offset.to_s,
|
|
102
|
+
ruby_ui__toaster_theme_value: @theme.to_s,
|
|
103
|
+
ruby_ui__toaster_rich_colors_value: @rich_colors.to_s,
|
|
104
|
+
ruby_ui__toaster_close_button_value: @close_button.to_s,
|
|
105
|
+
ruby_ui__toaster_hotkey_value: Array(@hotkey).join("+"),
|
|
106
|
+
ruby_ui__toaster_dir_value: @dir.to_s
|
|
107
|
+
},
|
|
108
|
+
class: region_classes
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def region_classes
|
|
113
|
+
<<~CLASSES.tr("\n", " ").squeeze(" ").strip
|
|
114
|
+
group/toaster pointer-events-none fixed z-[100] p-4 sm:p-6
|
|
115
|
+
data-[position=top-left]:top-0 data-[position=top-left]:left-0
|
|
116
|
+
data-[position=top-center]:top-0 data-[position=top-center]:left-1/2 data-[position=top-center]:-translate-x-1/2
|
|
117
|
+
data-[position=top-right]:top-0 data-[position=top-right]:right-0
|
|
118
|
+
data-[position=bottom-left]:bottom-0 data-[position=bottom-left]:left-0
|
|
119
|
+
data-[position=bottom-center]:bottom-0 data-[position=bottom-center]:left-1/2 data-[position=bottom-center]:-translate-x-1/2
|
|
120
|
+
data-[position=bottom-right]:bottom-0 data-[position=bottom-right]:right-0
|
|
121
|
+
CLASSES
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class ToastTitle < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
data: {slot: "title"},
|
|
14
|
+
class: "font-medium leading-normal"
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|