quicksilver_ui 0.1.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 +7 -0
- data/app/assets/tailwind/alert.css +35 -0
- data/app/assets/tailwind/badge.css +27 -0
- data/app/assets/tailwind/button.css +35 -0
- data/app/assets/tailwind/form.css +35 -0
- data/app/assets/tailwind/link.css +23 -0
- data/app/assets/tailwind/modal.css +43 -0
- data/app/assets/tailwind/quicksilver_ui/engine.css +7 -0
- data/app/assets/tailwind/typography.css +112 -0
- data/app/helpers/app_form_builder.rb +94 -0
- data/app/helpers/app_form_helper.rb +7 -0
- data/app/javascript/controllers/autogrow_controller.js +19 -0
- data/app/javascript/controllers/dismissable_controller.js +35 -0
- data/app/javascript/controllers/dropdown_controller.js +59 -0
- data/app/javascript/controllers/modal_controller.js +45 -0
- data/app/javascript/controllers/tabs_controller.js +62 -0
- data/app/javascript/mixins/use_floating_ui.js +104 -0
- data/app/views/form/base_tag.rb +42 -0
- data/app/views/form/checkbox.rb +62 -0
- data/app/views/form/date_field.rb +11 -0
- data/app/views/form/email_field.rb +7 -0
- data/app/views/form/error.rb +15 -0
- data/app/views/form/file_field.rb +12 -0
- data/app/views/form/group.rb +97 -0
- data/app/views/form/hint.rb +15 -0
- data/app/views/form/input.rb +11 -0
- data/app/views/form/label.rb +19 -0
- data/app/views/form/password_field.rb +7 -0
- data/app/views/form/phone_field.rb +7 -0
- data/app/views/form/radio_button.rb +37 -0
- data/app/views/form/search_field.rb +7 -0
- data/app/views/form/select.rb +46 -0
- data/app/views/form/text_field.rb +7 -0
- data/app/views/form/textarea.rb +27 -0
- data/app/views/form/toggle.rb +35 -0
- data/app/views/ui/accordion.rb +67 -0
- data/app/views/ui/alert.rb +84 -0
- data/app/views/ui/avatar.rb +57 -0
- data/app/views/ui/badge.rb +35 -0
- data/app/views/ui/base.rb +29 -0
- data/app/views/ui/dropdown/item.rb +49 -0
- data/app/views/ui/dropdown.rb +111 -0
- data/app/views/ui/icon.rb +46 -0
- data/app/views/ui/modal.rb +96 -0
- data/app/views/ui/toast.rb +90 -0
- data/lib/generators/quicksilver_ui/affordance/affordance_generator.rb +102 -0
- data/lib/generators/quicksilver_ui/component/all_generator.rb +32 -0
- data/lib/generators/quicksilver_ui/component/component_generator.rb +194 -0
- data/lib/generators/quicksilver_ui/form/all_generator.rb +32 -0
- data/lib/generators/quicksilver_ui/form/form_generator.rb +164 -0
- data/lib/generators/quicksilver_ui/form/templates/app_form_builder.rb +39 -0
- data/lib/generators/quicksilver_ui/form/templates/app_form_helper.rb +7 -0
- data/lib/generators/quicksilver_ui/install/install_generator.rb +42 -0
- data/lib/generators/quicksilver_ui/install/templates/base.rb +29 -0
- data/lib/generators/quicksilver_ui/install/templates/initializer.rb +16 -0
- data/lib/quicksilver_ui/dependencies.rb +191 -0
- data/lib/quicksilver_ui/engine.rb +18 -0
- data/lib/quicksilver_ui/version.rb +5 -0
- data/lib/quicksilver_ui.rb +37 -0
- metadata +98 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Alert < UI::Base
|
|
4
|
+
prop :icon, _Union?(String, Symbol), predicate: :private, reader: :private
|
|
5
|
+
prop :heading, String, reader: :private
|
|
6
|
+
prop :description, _String?, predicate: :private, reader: :private
|
|
7
|
+
prop :variant, _Union("neutral", "brand", "success", "warning", "danger", "info"),
|
|
8
|
+
default: :info, reader: :private do |value|
|
|
9
|
+
value.to_s.inquiry
|
|
10
|
+
end
|
|
11
|
+
prop :size, _Union("sm", "md"),
|
|
12
|
+
default: :md, reader: :private do |value|
|
|
13
|
+
value.to_s.inquiry
|
|
14
|
+
end
|
|
15
|
+
prop :dismissable, _Boolean, default: false, predicate: :private, reader: :private
|
|
16
|
+
|
|
17
|
+
def view_template
|
|
18
|
+
div(class: classes, data: data_with_defaults, role: alert_role) do
|
|
19
|
+
render Icon(name: icon, size:, class: "shrink-0 mt-1 #{icon_color_classes}") if icon?
|
|
20
|
+
div(class: "w-full") do
|
|
21
|
+
div(class: "flex items-center justify-between gap-2") do
|
|
22
|
+
h5(class: heading_classes) { heading }
|
|
23
|
+
|
|
24
|
+
if dismissable?
|
|
25
|
+
button aria_label: "Dismiss", data: {action: "dismissable#dismiss"} do
|
|
26
|
+
render UI::Icon.new(name: :x_mark, size:, class: "cursor-pointer #{icon_color_classes}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
p(class: description_classes) { description } if description?
|
|
32
|
+
|
|
33
|
+
if block_given?
|
|
34
|
+
div(class: description_classes) do
|
|
35
|
+
yield
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def default_classes
|
|
45
|
+
"ui-alert ui-alert-#{variant} ui-alert-#{size}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def icon_color_classes
|
|
49
|
+
class_names(
|
|
50
|
+
"text-turqoise-900": variant.brand?,
|
|
51
|
+
"text-emerald-800": variant.success?,
|
|
52
|
+
"text-amber-800": variant.warning?,
|
|
53
|
+
"text-red-800": variant.danger?,
|
|
54
|
+
"text-blue-800": variant.info?
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def heading_classes
|
|
59
|
+
class_names(
|
|
60
|
+
"text-gray-950",
|
|
61
|
+
"ui-text-md": size.md?,
|
|
62
|
+
"ui-text-sm": size.sm?,
|
|
63
|
+
"font-medium": description?
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def description_classes
|
|
68
|
+
class_names(
|
|
69
|
+
"text-gray-950",
|
|
70
|
+
"ui-text-md": size.md?,
|
|
71
|
+
"ui-text-sm": size.sm?
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def data_with_defaults
|
|
76
|
+
data.with_defaults(
|
|
77
|
+
dismissable? ? {controller: "dismissable"} : {}
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def alert_role
|
|
82
|
+
(variant.warning? || variant.danger?) ? :alert : :status
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Avatar < UI::Base
|
|
4
|
+
include Phlex::Rails::Helpers::ImageTag
|
|
5
|
+
|
|
6
|
+
prop :initials, String, reader: :private
|
|
7
|
+
prop :image_url, _Nilable(String), reader: :private
|
|
8
|
+
prop :size, _Union("xs", "sm", "md", "lg", "xl", "xxl"), default: :md, reader: :private do |value|
|
|
9
|
+
value.to_s.inquiry
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def view_template
|
|
13
|
+
span(class: classes) do
|
|
14
|
+
if image_url.present?
|
|
15
|
+
image_tag(image_url, class: "size-full rounded-full object-cover")
|
|
16
|
+
else
|
|
17
|
+
span(class: text_classes) { initials }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def classes
|
|
25
|
+
class_names(
|
|
26
|
+
super,
|
|
27
|
+
size_classes
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def default_classes
|
|
32
|
+
"inline-flex size-6 items-center justify-center rounded-full bg-primary-100 border border-primary-700"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def size_classes
|
|
36
|
+
class_names(
|
|
37
|
+
"size-6": size.xs?,
|
|
38
|
+
"size-8": size.sm?,
|
|
39
|
+
"size-10": size.md?,
|
|
40
|
+
"size-12": size.lg?,
|
|
41
|
+
"size-14": size.xl?,
|
|
42
|
+
"size-16": size.xxl?
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def text_classes
|
|
47
|
+
class_names(
|
|
48
|
+
"font-medium text-primary-900 uppercase",
|
|
49
|
+
"text-xs": size.xs?,
|
|
50
|
+
"text-sm": size.sm?,
|
|
51
|
+
"text-base": size.md?,
|
|
52
|
+
"text-xl": size.lg? || size.xl?,
|
|
53
|
+
"text-2xl": size.xxl?,
|
|
54
|
+
hidden: image_url.present?
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Badge < UI::Base
|
|
4
|
+
prop :text, String, reader: :private
|
|
5
|
+
prop :icon, _Nilable(Symbol), reader: :private
|
|
6
|
+
prop :variant, _Union("neutral", "brand", "danger"),
|
|
7
|
+
default: :neutral, reader: :private do |value|
|
|
8
|
+
value.to_s.inquiry
|
|
9
|
+
end
|
|
10
|
+
prop :size, _Union("sm", "md", "lg"),
|
|
11
|
+
default: :md, reader: :private do |value|
|
|
12
|
+
value.to_s.inquiry
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def view_template
|
|
16
|
+
div class: classes, data: do
|
|
17
|
+
UI::Icon(name: icon, class: icon_classes, size: :sm) if icon.present?
|
|
18
|
+
span { text }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def default_classes
|
|
25
|
+
"ui-badge ui-badge-#{variant} ui-badge-#{size}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def icon_classes
|
|
29
|
+
class_names(
|
|
30
|
+
"text-gray-700": variant.neutral?,
|
|
31
|
+
"text-primary-700": variant.brand?,
|
|
32
|
+
"text-red-400": variant.danger?
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Base < Phlex::HTML
|
|
4
|
+
TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER)
|
|
5
|
+
|
|
6
|
+
extend Literal::Properties
|
|
7
|
+
include Phlex::Rails::Helpers::Routes
|
|
8
|
+
include Phlex::Rails::Helpers::ClassNames
|
|
9
|
+
include Phlex::Rails::Helpers::LinkTo
|
|
10
|
+
|
|
11
|
+
if Rails.env.development?
|
|
12
|
+
def before_template
|
|
13
|
+
comment { "Before #{self.class.name}" }
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
prop :class, _Nilable(String)
|
|
19
|
+
prop :data, Hash, default: {}.freeze, reader: :private
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def classes
|
|
24
|
+
TAILWIND_MERGER.merge [default_classes, @class].join(" ")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def default_classes
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Dropdown::Item < UI::Base
|
|
4
|
+
prop :href, String, reader: :private
|
|
5
|
+
prop :icon, _Nilable(Symbol), reader: :private
|
|
6
|
+
prop :trailing_icon, _Nilable(Symbol), reader: :private
|
|
7
|
+
prop :method, _Union(Symbol, String), default: :get, reader: :private
|
|
8
|
+
|
|
9
|
+
def view_template(&)
|
|
10
|
+
tag do
|
|
11
|
+
render Icon(name: icon, class: "text-gray-500") if icon?
|
|
12
|
+
|
|
13
|
+
div class: text_classes do
|
|
14
|
+
yield
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
render Icon(name: trailing_icon, class: "text-gray-500") if trailing_icon?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def tag(&)
|
|
24
|
+
if method == :get
|
|
25
|
+
a href:, class: classes, &
|
|
26
|
+
else
|
|
27
|
+
button_to href, method:, form_class: classes, class: "flex items-center gap-3 w-full", &
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def icon?
|
|
32
|
+
icon.present?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def trailing_icon?
|
|
36
|
+
trailing_icon.present?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def text_classes
|
|
40
|
+
"ui-text-sm flex-1"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def default_classes
|
|
44
|
+
class_names(
|
|
45
|
+
"self-stretch flex justify-start items-center flex-1 px-4 py-2 gap-3",
|
|
46
|
+
"hover:bg-gray-100 focus:bg-gray-100 active:bg-gray-200"
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Dropdown < UI::Base
|
|
4
|
+
prop :heading, _Nilable(String), reader: :private
|
|
5
|
+
prop :offset, Integer, default: 8, reader: :private
|
|
6
|
+
prop :placement,
|
|
7
|
+
_Union("top", "top-start", "top-end", "right", "right-start", "right-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end"),
|
|
8
|
+
reader: :private, default: "bottom-end"
|
|
9
|
+
|
|
10
|
+
def initialize(**props)
|
|
11
|
+
super
|
|
12
|
+
@items = []
|
|
13
|
+
@trigger_block = nil
|
|
14
|
+
@action_block = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def item(href:, icon: nil, trailing_icon: nil, method: :get, &block)
|
|
18
|
+
@items << {href:, icon:, trailing_icon:, method:, block:}
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def action(&block)
|
|
23
|
+
@action_block = block if block_given?
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def trigger(class: nil, &block)
|
|
28
|
+
@trigger_block = block
|
|
29
|
+
@trigger_classes = grab(class:)
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def view_template(&block)
|
|
34
|
+
vanish(&block) if block_given?
|
|
35
|
+
|
|
36
|
+
div(data: {controller: "dropdown", dropdown_offset_value: offset, dropdown_placement_value: placement}, class: classes) do
|
|
37
|
+
render_trigger
|
|
38
|
+
|
|
39
|
+
div(
|
|
40
|
+
data: {
|
|
41
|
+
dropdown_target: "menu",
|
|
42
|
+
transition_enter_from: "opacity-0 scale-95",
|
|
43
|
+
transition_enter_to: "opacity-100 scale-100",
|
|
44
|
+
transition_leave_from: "opacity-100 scale-100",
|
|
45
|
+
transition_leave_to: "opacity-0 scale-95"
|
|
46
|
+
},
|
|
47
|
+
class:
|
|
48
|
+
"hidden transition transform origin-top-left absolute left-0 top-0 z-50"
|
|
49
|
+
) do
|
|
50
|
+
div(class: "w-64 bg-white shadow-xs border border-gray-300
|
|
51
|
+
inline-flex flex-col justify-start items-start overflow-hidden") do
|
|
52
|
+
render_heading
|
|
53
|
+
|
|
54
|
+
div(class: "w-full flex flex-col justify-start items-start gap-1") do
|
|
55
|
+
@items.each do |item_data|
|
|
56
|
+
render Dropdown::Item.new(
|
|
57
|
+
**item_data.except(:block),
|
|
58
|
+
&item_data[:block]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def render_trigger
|
|
70
|
+
button(class: trigger_classes, data: {action: "dropdown#toggle click@window->dropdown#hide"}) do
|
|
71
|
+
@trigger_block.call
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def render_heading
|
|
76
|
+
return unless heading.present? || action?
|
|
77
|
+
|
|
78
|
+
div(class: "w-full px-4 py-2 border-b border-gray-300 flex justify-between
|
|
79
|
+
items-center gap-4") do
|
|
80
|
+
if heading.present?
|
|
81
|
+
div(class: "ui-text-sm-medium") do
|
|
82
|
+
heading
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
render_action
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def render_action
|
|
91
|
+
div(class: "ui-text-xs text-gray-700") do
|
|
92
|
+
@action_block&.call
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def default_classes
|
|
97
|
+
"relative"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def trigger_classes
|
|
101
|
+
TAILWIND_MERGER.merge [default_trigger_classes, @trigger_classes].join(" ")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def default_trigger_classes
|
|
105
|
+
"focus:outline-none active:outline-none"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def action?
|
|
109
|
+
@action_block.present?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Icon < UI::Base
|
|
4
|
+
register_output_helper :icon
|
|
5
|
+
|
|
6
|
+
prop :size, _Union("xs", "sm", "md", "lg", "xl", "xxl"), default: :md, reader: :private do |value|
|
|
7
|
+
value.to_s.inquiry
|
|
8
|
+
end
|
|
9
|
+
prop :variant, _Union("micro", "mini", "outline", "solid"), default: :outline, reader: :private do |value|
|
|
10
|
+
value.to_s.inquiry
|
|
11
|
+
end
|
|
12
|
+
prop :name, String, reader: :private do |value|
|
|
13
|
+
value.to_s.dasherize
|
|
14
|
+
end
|
|
15
|
+
prop :fixed_width, _Boolean, default: true, reader: :private
|
|
16
|
+
prop :spin, _Boolean, default: false, reader: :private
|
|
17
|
+
|
|
18
|
+
def view_template
|
|
19
|
+
icon(name, class: classes, variant:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def size_classes
|
|
25
|
+
class_names(
|
|
26
|
+
"size-2": size.xs?,
|
|
27
|
+
"size-3": size.sm?,
|
|
28
|
+
"size-4": size.md?,
|
|
29
|
+
"size-6": size.lg?,
|
|
30
|
+
"size-8": size.xl?,
|
|
31
|
+
"size-10": size.xxl?
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def spin_classes
|
|
36
|
+
"animate-spin" if spin
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def default_classes
|
|
40
|
+
class_names(
|
|
41
|
+
"text-inherit",
|
|
42
|
+
size_classes,
|
|
43
|
+
spin_classes
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Modal < UI::Base
|
|
4
|
+
prop :open, _Boolean, default: false, reader: :private
|
|
5
|
+
prop :heading, String, reader: :private
|
|
6
|
+
prop :text, _Nilable(String), reader: :private
|
|
7
|
+
prop :backdrop_closeable, _Boolean, default: true, reader: :private
|
|
8
|
+
|
|
9
|
+
def initialize(**props)
|
|
10
|
+
super
|
|
11
|
+
@trigger_block = nil
|
|
12
|
+
@footer_block = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def view_template(&block)
|
|
16
|
+
vanish(&block) if block_given?
|
|
17
|
+
|
|
18
|
+
div data: {controller: "modal", modal_open_value: open}.merge(backdrop_closeable_attributes) do
|
|
19
|
+
dialog(data: {modal_target: "dialog"}, class: classes) do
|
|
20
|
+
div(class: "ui-modal-body") do
|
|
21
|
+
div(class: "flex justify-between items-start") do
|
|
22
|
+
render_heading
|
|
23
|
+
render_close_trigger
|
|
24
|
+
end
|
|
25
|
+
render_text
|
|
26
|
+
|
|
27
|
+
yield
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
render_footer
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
render_trigger
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def trigger(class: nil, &block)
|
|
38
|
+
@trigger_block = block
|
|
39
|
+
@trigger_classes = grab(class:)
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def footer(class: nil, &block)
|
|
44
|
+
@footer_block = block
|
|
45
|
+
@footer_classes = grab(class:)
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def render_close_trigger
|
|
52
|
+
button(data: {action: "modal#close"}, class: "cursor-pointer hover:text-gray-700") do
|
|
53
|
+
render UI::Icon.new(name: :x_mark)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_heading
|
|
58
|
+
h2(class: "ui-display-md") { heading }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_text
|
|
62
|
+
p(class: "ui-text-md") { text }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def render_footer
|
|
66
|
+
return if @footer_block.nil?
|
|
67
|
+
|
|
68
|
+
div(class: footer_classes) do
|
|
69
|
+
@footer_block.call
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def render_trigger
|
|
74
|
+
button(class: @trigger_classes, data: {action: "modal#open"}) do
|
|
75
|
+
@trigger_block.call
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def backdrop_closeable_attributes
|
|
80
|
+
return {action: "click->modal#backdropClose"} if backdrop_closeable?
|
|
81
|
+
|
|
82
|
+
{}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def backdrop_closeable?
|
|
86
|
+
backdrop_closeable
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def default_classes
|
|
90
|
+
"ui-modal"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def footer_classes
|
|
94
|
+
TAILWIND_MERGER.merge ["ui-modal-footer", @footer_classes].join(" ")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::Toast < UI::Base
|
|
4
|
+
AUTO_DISMISS_DELAY = 8000
|
|
5
|
+
|
|
6
|
+
prop :dismissable, _Boolean, default: true, reader: :private
|
|
7
|
+
prop :auto_dismiss, _Boolean, default: true, reader: :private
|
|
8
|
+
prop :variant, _Union("neutral", "danger"),
|
|
9
|
+
default: :neutral, reader: :private do |value|
|
|
10
|
+
value.to_s.inquiry
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(**props)
|
|
14
|
+
super
|
|
15
|
+
@action = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def view_template
|
|
19
|
+
output(
|
|
20
|
+
data: {
|
|
21
|
+
controller: "dismissable",
|
|
22
|
+
dismissable_auto_dismiss_time_value: auto_dismiss_delay
|
|
23
|
+
},
|
|
24
|
+
class: classes
|
|
25
|
+
) do
|
|
26
|
+
div class: "w-full min-h-8 py-1 px-2 inline-flex items-start justify-between gap-6" do
|
|
27
|
+
span(class: "mt-0.5") { yield }
|
|
28
|
+
|
|
29
|
+
span(class: "flex items-center gap-x-6") do
|
|
30
|
+
render_action
|
|
31
|
+
|
|
32
|
+
if dismissable?
|
|
33
|
+
button data: {action: "dismissable#dismiss"}, class: button_classes do
|
|
34
|
+
Icon(name: :x_mark, variant: :outline, class: "mt-px")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def action(href:, text:)
|
|
43
|
+
@action = {href:, text:}
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def render_action
|
|
50
|
+
return if @action.nil?
|
|
51
|
+
|
|
52
|
+
link_to @action[:text], @action[:href], class: action_classes
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def dismissable?
|
|
56
|
+
dismissable
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def auto_dismiss?
|
|
60
|
+
dismissable? && auto_dismiss
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def auto_dismiss_delay
|
|
64
|
+
AUTO_DISMISS_DELAY if auto_dismiss?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def default_classes
|
|
68
|
+
class_names(
|
|
69
|
+
"min-w-96 max-w-lg block text-sm font-medium ring shadow-xl",
|
|
70
|
+
"bg-gray-50 text-gray-900 ring-gray-300": variant.neutral?,
|
|
71
|
+
"bg-red-500 text-white ring-red-700": variant.danger?
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def button_classes
|
|
76
|
+
class_names(
|
|
77
|
+
"p-1 focus:outline-none",
|
|
78
|
+
"text-gray-900 hover:bg-gray-200 focus:bg-gray-200 active:bg-gray-300": variant.neutral?,
|
|
79
|
+
"text-white hover:bg-red-500 focus:bg-red-500 active:bg-red-400": variant.danger?
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def action_classes
|
|
84
|
+
class_names(
|
|
85
|
+
"py-0.5 px-1 focus:outline-none border text-xs",
|
|
86
|
+
"border-gray-700 hover:border-gray-900 hover:bg-gray-200 focus:border-gray-900 focus:bg-gray-200 active:bg-gray-300": variant.neutral?,
|
|
87
|
+
"border-red-300 hover:bg-red-600 hover:border-red-100 focus:bg-red-600 focus:border-red-100 active:bg-red-700 active:border-red-100": variant.danger?
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module QuicksilverUI
|
|
6
|
+
module Generators
|
|
7
|
+
class AffordanceGenerator < Rails::Generators::Base
|
|
8
|
+
namespace "quicksilver_ui:affordance"
|
|
9
|
+
|
|
10
|
+
source_root QuicksilverUI.stylesheets_path.to_s
|
|
11
|
+
|
|
12
|
+
def self.banner
|
|
13
|
+
"rails generate quicksilver_ui:affordance NAME [options]"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc <<~DESC
|
|
17
|
+
Generate a QuicksilverUI affordance stylesheet into your application.
|
|
18
|
+
|
|
19
|
+
Available affordances:
|
|
20
|
+
DESC
|
|
21
|
+
|
|
22
|
+
def self.desc(description = nil)
|
|
23
|
+
return super if description
|
|
24
|
+
|
|
25
|
+
affordances = Dir.glob(File.join(QuicksilverUI.stylesheets_path, "*.css"))
|
|
26
|
+
.map { |f| File.basename(f, ".css") }
|
|
27
|
+
.sort
|
|
28
|
+
.map { |c| " #{c}" }
|
|
29
|
+
.join("\n")
|
|
30
|
+
|
|
31
|
+
"#{super}\n#{affordances}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
argument :affordance_name, type: :string, required: true
|
|
35
|
+
class_option :force, type: :boolean, default: false
|
|
36
|
+
|
|
37
|
+
def generate_affordance
|
|
38
|
+
if affordance_not_found?
|
|
39
|
+
say "Affordance not found: #{affordance_name}", :red
|
|
40
|
+
say ""
|
|
41
|
+
say "Available affordances:", :green
|
|
42
|
+
available_affordances.each { |a| say " - #{a}" }
|
|
43
|
+
exit 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
say "Generating #{affordance_name} affordance..."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def copy_stylesheet
|
|
50
|
+
source = File.join(QuicksilverUI.stylesheets_path, "#{file_name}.css")
|
|
51
|
+
copy_file source, Rails.root.join("app/assets/tailwind", "#{file_name}.css"), force: options["force"]
|
|
52
|
+
add_css_import(file_name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def done
|
|
56
|
+
say ""
|
|
57
|
+
say "#{affordance_name} affordance generated!", :green
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def file_name
|
|
63
|
+
affordance_name.underscore
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def affordance_not_found?
|
|
67
|
+
!File.exist?(File.join(QuicksilverUI.stylesheets_path, "#{file_name}.css"))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def available_affordances
|
|
71
|
+
Dir.glob(File.join(QuicksilverUI.stylesheets_path, "*.css"))
|
|
72
|
+
.map { |f| File.basename(f, ".css") }
|
|
73
|
+
.sort
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def add_css_import(name)
|
|
77
|
+
app_css = Rails.root.join("app/assets/tailwind/application.css")
|
|
78
|
+
import_line = "@import \"./#{name}.css\" layer(affordances);"
|
|
79
|
+
|
|
80
|
+
if File.exist?(app_css)
|
|
81
|
+
content = File.read(app_css)
|
|
82
|
+
return if content.include?(import_line)
|
|
83
|
+
|
|
84
|
+
lines = content.lines
|
|
85
|
+
last_import_index = lines.rindex { |l| l.start_with?("@import") }
|
|
86
|
+
|
|
87
|
+
if last_import_index
|
|
88
|
+
lines.insert(last_import_index + 1, "#{import_line}\n")
|
|
89
|
+
else
|
|
90
|
+
lines.unshift("#{import_line}\n")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
File.write(app_css, lines.join)
|
|
94
|
+
else
|
|
95
|
+
create_file app_css, "#{import_line}\n"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
say " Added import for #{name}.css to application.css", :green
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|