view_primitives 0.1.3 → 0.2.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/CHANGELOG.md +40 -0
- data/README.md +57 -2
- data/lib/generators/view_primitives/add/add_generator.rb +8 -62
- data/lib/generators/view_primitives/add/templates/accordion/accordion_item_component.rb.tt +30 -11
- data/lib/generators/view_primitives/add/templates/alert/alert_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/alert_dialog/alert_dialog_component.rb.tt +9 -9
- data/lib/generators/view_primitives/add/templates/aspect_ratio/aspect_ratio_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/audio/audio_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/avatar/avatar_component.rb.tt +8 -4
- data/lib/generators/view_primitives/add/templates/badge/badge_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/banner/banner_component.rb.tt +6 -6
- data/lib/generators/view_primitives/add/templates/bottom_nav/bottom_nav_component.rb.tt +11 -4
- data/lib/generators/view_primitives/add/templates/breadcrumb/breadcrumb_component.rb.tt +2 -2
- data/lib/generators/view_primitives/add/templates/button/button_component.rb.tt +8 -5
- data/lib/generators/view_primitives/add/templates/button_group/button_group_component.rb.tt +5 -5
- data/lib/generators/view_primitives/add/templates/calendar/calendar_component.rb.tt +18 -16
- data/lib/generators/view_primitives/add/templates/card/card_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/card/card_footer_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/carousel/carousel_component.rb.tt +26 -13
- data/lib/generators/view_primitives/add/templates/chart/chart_component.rb.tt +10 -4
- data/lib/generators/view_primitives/add/templates/chart/chart_controller.js +26 -3
- data/lib/generators/view_primitives/add/templates/chat_bubble/chat_bubble_component.rb.tt +4 -4
- data/lib/generators/view_primitives/add/templates/checkbox/checkbox_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/collapsible/collapsible_component.rb.tt +12 -5
- data/lib/generators/view_primitives/add/templates/combobox/combobox_component.rb.tt +3 -6
- data/lib/generators/view_primitives/add/templates/command/command_component.rb.tt +22 -18
- data/lib/generators/view_primitives/add/templates/command/command_controller.js +50 -0
- data/lib/generators/view_primitives/add/templates/context_menu/context_menu_component.rb.tt +9 -8
- data/lib/generators/view_primitives/add/templates/data_table/data_table_component.rb.tt +60 -29
- data/lib/generators/view_primitives/add/templates/data_table/data_table_controller.js +2 -2
- data/lib/generators/view_primitives/add/templates/date_picker/date_picker_component.rb.tt +8 -8
- data/lib/generators/view_primitives/add/templates/device_mockup/device_mockup_component.rb.tt +94 -21
- data/lib/generators/view_primitives/add/templates/dialog/dialog_component.rb.tt +13 -10
- data/lib/generators/view_primitives/add/templates/dialog/dialog_controller.js +52 -0
- data/lib/generators/view_primitives/add/templates/drawer/drawer_component.rb.tt +8 -7
- data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_menu_component.rb.tt +5 -6
- data/lib/generators/view_primitives/add/templates/embed/embed_component.rb.tt +2 -2
- data/lib/generators/view_primitives/add/templates/figure/figure_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/file_input/file_input_component.rb.tt +3 -12
- data/lib/generators/view_primitives/add/templates/floating_label/floating_label_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/footer/footer_component.rb.tt +5 -4
- data/lib/generators/view_primitives/add/templates/form_field/form_field_component.rb.tt +18 -5
- data/lib/generators/view_primitives/add/templates/gallery/gallery_component.rb.tt +3 -3
- data/lib/generators/view_primitives/add/templates/gallery/gallery_controller.js +1 -1
- data/lib/generators/view_primitives/add/templates/hover_card/hover_card_component.rb.tt +6 -5
- data/lib/generators/view_primitives/add/templates/iframe/iframe_component.rb.tt +6 -4
- data/lib/generators/view_primitives/add/templates/image/image_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/indicator/indicator_component.rb.tt +5 -4
- data/lib/generators/view_primitives/add/templates/input/input_component.rb.tt +2 -13
- data/lib/generators/view_primitives/add/templates/input_otp/input_otp_component.rb.tt +22 -10
- data/lib/generators/view_primitives/add/templates/kbd/kbd_component.rb.tt +3 -1
- data/lib/generators/view_primitives/add/templates/list_group/list_group_component.rb.tt +6 -2
- data/lib/generators/view_primitives/add/templates/list_group/list_group_item_component.rb.tt +6 -4
- data/lib/generators/view_primitives/add/templates/map_area/map_area_component.rb.tt +3 -2
- data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_component.rb.tt +9 -9
- data/lib/generators/view_primitives/add/templates/menubar/menubar_component.rb.tt +5 -5
- data/lib/generators/view_primitives/add/templates/menubar/menubar_menu_component.rb.tt +4 -5
- data/lib/generators/view_primitives/add/templates/navbar/navbar_component.rb.tt +51 -11
- data/lib/generators/view_primitives/add/templates/navbar/navbar_controller.js +8 -3
- data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_component.rb.tt +12 -16
- data/lib/generators/view_primitives/add/templates/number_input/number_input_component.rb.tt +4 -11
- data/lib/generators/view_primitives/add/templates/pagination/pagination_component.rb.tt +4 -3
- data/lib/generators/view_primitives/add/templates/picture/picture_component.rb.tt +2 -1
- data/lib/generators/view_primitives/add/templates/popover/popover_component.rb.tt +1 -2
- data/lib/generators/view_primitives/add/templates/progress/progress_component.rb.tt +3 -1
- data/lib/generators/view_primitives/add/templates/qr_code/qr_code_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/radio_group/radio_group_component.rb.tt +8 -5
- data/lib/generators/view_primitives/add/templates/range/range_component.rb.tt +2 -3
- data/lib/generators/view_primitives/add/templates/rating/rating_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/rating_input/rating_controller.js +1 -1
- data/lib/generators/view_primitives/add/templates/rating_input/rating_input_component.rb.tt +4 -3
- data/lib/generators/view_primitives/add/templates/resizable/resizable_component.rb.tt +27 -15
- data/lib/generators/view_primitives/add/templates/scroll_area/scroll_area_component.rb.tt +10 -11
- data/lib/generators/view_primitives/add/templates/search_input/search_input_component.rb.tt +2 -11
- data/lib/generators/view_primitives/add/templates/select/select_component.rb.tt +25 -6
- data/lib/generators/view_primitives/add/templates/separator/separator_component.rb.tt +6 -3
- data/lib/generators/view_primitives/add/templates/sheet/sheet_component.rb.tt +25 -21
- data/lib/generators/view_primitives/add/templates/sidebar/sidebar_component.rb.tt +27 -21
- data/lib/generators/view_primitives/add/templates/skeleton/skeleton_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_component.rb.tt +8 -9
- data/lib/generators/view_primitives/add/templates/spinner/spinner_component.rb.tt +15 -6
- data/lib/generators/view_primitives/add/templates/stepper/stepper_component.rb.tt +17 -16
- data/lib/generators/view_primitives/add/templates/switch/switch_component.rb.tt +27 -14
- data/lib/generators/view_primitives/add/templates/tabs/tabs_component.html.erb +13 -7
- data/lib/generators/view_primitives/add/templates/tags_input/tags_input_component.rb.tt +136 -0
- data/lib/generators/view_primitives/add/templates/tags_input/tags_input_controller.js +90 -0
- data/lib/generators/view_primitives/add/templates/textarea/textarea_component.rb.tt +2 -11
- data/lib/generators/view_primitives/add/templates/timeline/timeline_component.rb.tt +9 -7
- data/lib/generators/view_primitives/add/templates/timepicker/timepicker_component.rb.tt +19 -15
- data/lib/generators/view_primitives/add/templates/toaster/toaster_component.rb.tt +10 -10
- data/lib/generators/view_primitives/add/templates/toaster/toaster_controller.js +6 -6
- data/lib/generators/view_primitives/add/templates/toggle/toggle_component.rb.tt +10 -3
- data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_component.rb.tt +6 -6
- data/lib/generators/view_primitives/add/templates/tooltip/tooltip_component.rb.tt +7 -6
- data/lib/generators/view_primitives/add/templates/video/video_component.rb.tt +1 -1
- data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_component.rb.tt +9 -3
- data/lib/generators/view_primitives/component_copier.rb +96 -0
- data/lib/generators/view_primitives/components.rb +16 -2
- data/lib/generators/view_primitives/install/install_generator.rb +13 -3
- data/lib/generators/view_primitives/install/templates/application_component.rb.tt +7 -0
- data/lib/generators/view_primitives/install/templates/styles.rb.tt +26 -0
- data/lib/generators/view_primitives/install/templates/view_primitives/themes/default.css +79 -0
- data/lib/generators/view_primitives/install/templates/view_primitives/themes/rose.css +57 -0
- data/lib/generators/view_primitives/install/templates/view_primitives/tokens.css +46 -0
- data/lib/generators/view_primitives/install/templates/view_primitives/utilities.css +64 -0
- data/lib/generators/view_primitives/install/templates/view_primitives.css +6 -66
- data/lib/generators/view_primitives/list/list_generator.rb +3 -1
- data/lib/generators/view_primitives/theme/theme_generator.rb +79 -0
- data/lib/generators/view_primitives/update/update_generator.rb +112 -0
- data/lib/view_primitives/class_helper.rb +4 -1
- data/lib/view_primitives/railtie.rb +1 -1
- data/lib/view_primitives/version.rb +1 -1
- metadata +12 -4
- data/lib/generators/view_primitives/add/templates/drawer/drawer_controller.js +0 -15
- data/lib/generators/view_primitives/add/templates/sheet/sheet_controller.js +0 -15
data/lib/generators/view_primitives/add/templates/device_mockup/device_mockup_component.rb.tt
CHANGED
|
@@ -2,24 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class DeviceMockupComponent < ApplicationComponent
|
|
5
|
+
# Shell holds aspect ratio and hardware controls; bezel clips the screen inset.
|
|
6
|
+
PHONE_SHELL = "relative mx-auto w-full max-w-[280px] aspect-[393/852]"
|
|
7
|
+
|
|
8
|
+
PHONE_BEZEL = "absolute inset-0 overflow-hidden rounded-[3rem] bg-gradient-to-b from-zinc-600 via-zinc-800 to-zinc-900 " \
|
|
9
|
+
"shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5)] ring-1 ring-inset ring-white/15"
|
|
10
|
+
|
|
11
|
+
PHONE_SCREEN = "absolute inset-[10px] flex flex-col overflow-hidden rounded-[2.45rem] bg-black"
|
|
12
|
+
|
|
13
|
+
DYNAMIC_ISLAND = "pointer-events-none absolute left-1/2 top-[10px] z-20 h-[26px] w-[92px] " \
|
|
14
|
+
"-translate-x-1/2 rounded-full bg-zinc-950 " \
|
|
15
|
+
"shadow-[0_0_0_1px_rgba(255,255,255,0.14),inset_0_1px_0_rgba(255,255,255,0.08)]"
|
|
16
|
+
|
|
17
|
+
HOME_INDICATOR = "pointer-events-none absolute bottom-[7px] left-1/2 z-20 h-1 w-[112px] " \
|
|
18
|
+
"-translate-x-1/2 rounded-full bg-white/40"
|
|
19
|
+
|
|
20
|
+
PHONE_BUTTONS = [
|
|
21
|
+
"pointer-events-none absolute -left-px top-[24%] z-30 h-7 w-[3px] rounded-l-sm bg-zinc-500/90",
|
|
22
|
+
"pointer-events-none absolute -left-px top-[31%] z-30 h-11 w-[3px] rounded-l-sm bg-zinc-500/90",
|
|
23
|
+
"pointer-events-none absolute -left-px top-[40%] z-30 h-11 w-[3px] rounded-l-sm bg-zinc-500/90",
|
|
24
|
+
"pointer-events-none absolute -right-px top-[33%] z-30 h-14 w-[3px] rounded-r-sm bg-zinc-500/90"
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
TABLET_SHELL = "relative mx-auto w-full max-w-[640px] aspect-[3/2]"
|
|
28
|
+
|
|
29
|
+
TABLET_BEZEL = "absolute inset-0 overflow-hidden rounded-[1.5rem] bg-gradient-to-b from-zinc-600 via-zinc-800 to-zinc-900 " \
|
|
30
|
+
"shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5)] ring-1 ring-inset ring-white/15"
|
|
31
|
+
|
|
32
|
+
TABLET_SCREEN = "absolute inset-3 flex flex-col overflow-hidden rounded-[0.85rem] bg-black"
|
|
33
|
+
|
|
34
|
+
TABLET_CAMERA = "pointer-events-none absolute left-1/2 top-[6px] z-30 size-[7px] " \
|
|
35
|
+
"-translate-x-1/2 rounded-full bg-zinc-950 " \
|
|
36
|
+
"shadow-[0_0_0_1px_rgba(255,255,255,0.1),inset_0_0_2px_rgba(0,0,0,0.9)]"
|
|
37
|
+
|
|
38
|
+
SCREEN_CONTENT = "relative flex min-h-0 flex-1 flex-col"
|
|
39
|
+
|
|
5
40
|
VARIANTS = {
|
|
6
41
|
phone: {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
screen:
|
|
10
|
-
|
|
42
|
+
shell: PHONE_SHELL,
|
|
43
|
+
bezel: PHONE_BEZEL,
|
|
44
|
+
screen: PHONE_SCREEN,
|
|
45
|
+
side_controls: PHONE_BUTTONS,
|
|
46
|
+
overlays: [DYNAMIC_ISLAND, HOME_INDICATOR]
|
|
11
47
|
},
|
|
12
48
|
browser: {
|
|
13
|
-
outer: "relative mx-auto
|
|
14
|
-
|
|
49
|
+
outer: "relative mx-auto flex w-full max-w-3xl flex-col overflow-hidden rounded-lg " \
|
|
50
|
+
"#{UI::Styles::BORDER} bg-background shadow-md",
|
|
51
|
+
bar: "flex h-10 shrink-0 items-center gap-2 border-b border-border bg-muted/50 px-4",
|
|
15
52
|
dots: "flex gap-1.5",
|
|
16
|
-
screen: "overflow-hidden bg-
|
|
53
|
+
screen: "relative w-full min-h-48 overflow-hidden bg-background"
|
|
17
54
|
},
|
|
18
55
|
tablet: {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
screen:
|
|
22
|
-
|
|
56
|
+
shell: TABLET_SHELL,
|
|
57
|
+
bezel: TABLET_BEZEL,
|
|
58
|
+
screen: TABLET_SCREEN,
|
|
59
|
+
bezel_overlays: [TABLET_CAMERA],
|
|
60
|
+
overlays: [HOME_INDICATOR]
|
|
23
61
|
}
|
|
24
62
|
}.freeze
|
|
25
63
|
|
|
@@ -35,29 +73,64 @@ module UI
|
|
|
35
73
|
def call
|
|
36
74
|
cfg = VARIANTS.fetch(@variant, VARIANTS[:phone])
|
|
37
75
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
else
|
|
43
|
-
concat content_tag(:div, nil, class: cfg[:notch]) if cfg[:notch]
|
|
44
|
-
concat content_tag(:div, content, class: cfg[:screen])
|
|
45
|
-
end
|
|
76
|
+
if @variant == :browser
|
|
77
|
+
browser_mockup(cfg)
|
|
78
|
+
else
|
|
79
|
+
device_mockup(cfg)
|
|
46
80
|
end
|
|
47
81
|
end
|
|
48
82
|
|
|
49
83
|
private
|
|
50
84
|
|
|
85
|
+
def device_mockup(cfg)
|
|
86
|
+
content_tag(:div, class: cn(cfg[:shell], @extra_class), **@html_attrs) do
|
|
87
|
+
render_side_controls(cfg)
|
|
88
|
+
concat(content_tag(:div, class: cfg[:bezel]) do
|
|
89
|
+
render_bezel_overlays(cfg)
|
|
90
|
+
concat device_screen(cfg)
|
|
91
|
+
end)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def browser_mockup(cfg)
|
|
96
|
+
content_tag(:div, class: cn(cfg[:outer], @extra_class), **@html_attrs) do
|
|
97
|
+
concat browser_bar(cfg)
|
|
98
|
+
concat content_tag(:div, content, class: cfg[:screen])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def render_side_controls(cfg)
|
|
103
|
+
Array(cfg[:side_controls]).each do |cls|
|
|
104
|
+
concat content_tag(:div, nil, class: cls, role: "presentation")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def render_bezel_overlays(cfg)
|
|
109
|
+
Array(cfg[:bezel_overlays]).each do |cls|
|
|
110
|
+
concat content_tag(:div, nil, class: cls, role: "presentation")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def device_screen(cfg)
|
|
115
|
+
content_tag(:div, class: cfg[:screen]) do
|
|
116
|
+
Array(cfg[:overlays]).each do |cls|
|
|
117
|
+
concat content_tag(:div, nil, class: cls, role: "presentation")
|
|
118
|
+
end
|
|
119
|
+
concat content_tag(:div, class: SCREEN_CONTENT) { concat content }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
51
123
|
def browser_bar(cfg)
|
|
52
124
|
content_tag(:div, class: cfg[:bar]) do
|
|
53
125
|
concat(content_tag(:div, class: cfg[:dots]) {
|
|
54
|
-
%w[bg-
|
|
126
|
+
%w[bg-[#FF5F57] bg-[#FFBD2E] bg-[#28CA42]].each do |color|
|
|
55
127
|
concat content_tag(:div, nil, class: "size-3 rounded-full #{color}")
|
|
56
128
|
end
|
|
57
129
|
})
|
|
58
130
|
if @url
|
|
59
131
|
concat content_tag(:div, @url,
|
|
60
|
-
class: "ml-4 flex-1 truncate rounded-md bg-
|
|
132
|
+
class: "ml-4 flex-1 truncate rounded-md border border-input bg-transparent px-3 py-1 " \
|
|
133
|
+
"text-xs text-muted-foreground shadow-xs dark:bg-input/30")
|
|
61
134
|
end
|
|
62
135
|
end
|
|
63
136
|
end
|
|
@@ -5,10 +5,13 @@ module UI
|
|
|
5
5
|
renders_one :trigger
|
|
6
6
|
renders_one :footer
|
|
7
7
|
|
|
8
|
-
OVERLAY =
|
|
9
|
-
PANEL = "fixed
|
|
8
|
+
OVERLAY = UI::Styles::OVERLAY
|
|
9
|
+
PANEL = "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] " \
|
|
10
10
|
"translate-x-[-50%] translate-y-[-50%] gap-4 " \
|
|
11
|
-
"rounded-lg
|
|
11
|
+
"rounded-lg #{UI::Styles::BORDER} bg-background p-6 shadow-lg duration-200 outline-none sm:max-w-lg"
|
|
12
|
+
CLOSE_BTN = "absolute top-4 right-4 z-10 rounded-xs opacity-70 ring-offset-background transition-opacity " \
|
|
13
|
+
"hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden " \
|
|
14
|
+
"disabled:pointer-events-none"
|
|
12
15
|
|
|
13
16
|
def initialize(title: nil, description: nil, **html_attrs)
|
|
14
17
|
@title = title
|
|
@@ -40,8 +43,8 @@ module UI
|
|
|
40
43
|
data: { action: "keydown.escape@window->dialog#close" }) {
|
|
41
44
|
concat close_button
|
|
42
45
|
concat header_area
|
|
43
|
-
concat content_tag(:div, content
|
|
44
|
-
concat content_tag(:div, footer, class: "
|
|
46
|
+
concat content_tag(:div, content) unless content.blank?
|
|
47
|
+
concat content_tag(:div, footer, class: "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end") if footer
|
|
45
48
|
}
|
|
46
49
|
end
|
|
47
50
|
end
|
|
@@ -49,9 +52,9 @@ module UI
|
|
|
49
52
|
def header_area
|
|
50
53
|
return "" if @title.nil? && @description.nil?
|
|
51
54
|
|
|
52
|
-
content_tag(:div, class: "
|
|
53
|
-
concat content_tag(:h2, @title, class: "text-lg
|
|
54
|
-
concat content_tag(:p, @description, class: "
|
|
55
|
+
content_tag(:div, class: "flex flex-col gap-2 text-center sm:text-left") do
|
|
56
|
+
concat content_tag(:h2, @title, class: "text-lg leading-none font-semibold") if @title
|
|
57
|
+
concat content_tag(:p, @description, class: "text-sm text-muted-foreground") if @description
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
60
|
|
|
@@ -59,13 +62,13 @@ module UI
|
|
|
59
62
|
content_tag(:button,
|
|
60
63
|
close_svg,
|
|
61
64
|
type: "button",
|
|
62
|
-
class:
|
|
65
|
+
class: CLOSE_BTN,
|
|
63
66
|
data: { action: "click->dialog#close" },
|
|
64
67
|
"aria-label": "Close")
|
|
65
68
|
end
|
|
66
69
|
|
|
67
70
|
def close_svg
|
|
68
|
-
raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>')
|
|
71
|
+
raw('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>')
|
|
69
72
|
end
|
|
70
73
|
end
|
|
71
74
|
end
|
|
@@ -6,10 +6,62 @@ export default class extends Controller {
|
|
|
6
6
|
open() {
|
|
7
7
|
this.panelTarget.hidden = false
|
|
8
8
|
document.body.style.overflow = "hidden"
|
|
9
|
+
|
|
10
|
+
this._previouslyFocused = document.activeElement
|
|
11
|
+
this._dialog = this.panelTarget.querySelector("[role=dialog], [role=alertdialog]")
|
|
12
|
+
this._trapFocus = this.#trapFocus.bind(this)
|
|
13
|
+
this._dialog?.addEventListener("keydown", this._trapFocus)
|
|
14
|
+
|
|
15
|
+
const focusable = this.#focusableElements(this._dialog)
|
|
16
|
+
if (focusable.length > 0) {
|
|
17
|
+
focusable[0].focus()
|
|
18
|
+
} else {
|
|
19
|
+
this._dialog?.focus()
|
|
20
|
+
}
|
|
9
21
|
}
|
|
10
22
|
|
|
11
23
|
close() {
|
|
12
24
|
this.panelTarget.hidden = true
|
|
13
25
|
document.body.style.overflow = ""
|
|
26
|
+
|
|
27
|
+
this._dialog?.removeEventListener("keydown", this._trapFocus)
|
|
28
|
+
this._previouslyFocused?.focus?.()
|
|
29
|
+
this._previouslyFocused = null
|
|
30
|
+
this._dialog = null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#trapFocus(event) {
|
|
34
|
+
if (event.key !== "Tab" || !this._dialog) return
|
|
35
|
+
|
|
36
|
+
const focusable = this.#focusableElements(this._dialog)
|
|
37
|
+
if (focusable.length === 0) return
|
|
38
|
+
|
|
39
|
+
const first = focusable[0]
|
|
40
|
+
const last = focusable[focusable.length - 1]
|
|
41
|
+
|
|
42
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
43
|
+
event.preventDefault()
|
|
44
|
+
last.focus()
|
|
45
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
46
|
+
event.preventDefault()
|
|
47
|
+
first.focus()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#focusableElements(container) {
|
|
52
|
+
if (!container) return []
|
|
53
|
+
|
|
54
|
+
const selector = [
|
|
55
|
+
"a[href]",
|
|
56
|
+
"button:not([disabled])",
|
|
57
|
+
"input:not([disabled])",
|
|
58
|
+
"select:not([disabled])",
|
|
59
|
+
"textarea:not([disabled])",
|
|
60
|
+
"[tabindex]:not([tabindex='-1'])",
|
|
61
|
+
].join(", ")
|
|
62
|
+
|
|
63
|
+
return Array.from(container.querySelectorAll(selector)).filter(
|
|
64
|
+
(el) => !el.hasAttribute("hidden") && el.offsetParent !== null
|
|
65
|
+
)
|
|
14
66
|
}
|
|
15
67
|
}
|
|
@@ -5,8 +5,9 @@ module UI
|
|
|
5
5
|
renders_one :trigger
|
|
6
6
|
renders_one :footer
|
|
7
7
|
|
|
8
|
-
OVERLAY =
|
|
9
|
-
PANEL = "fixed inset-x-0 bottom-0 z-50
|
|
8
|
+
OVERLAY = UI::Styles::OVERLAY
|
|
9
|
+
PANEL = "fixed inset-x-0 bottom-0 z-50 flex h-auto max-h-[80vh] flex-col " \
|
|
10
|
+
"rounded-t-lg border-t border-border bg-background shadow-lg overflow-y-auto"
|
|
10
11
|
|
|
11
12
|
def initialize(title: nil, description: nil, **html_attrs)
|
|
12
13
|
@title = title
|
|
@@ -16,8 +17,8 @@ module UI
|
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def call
|
|
19
|
-
content_tag(:div, data: { controller: "
|
|
20
|
-
concat content_tag(:span, trigger, data: { action: "click->
|
|
20
|
+
content_tag(:div, data: { controller: "dialog" }, **@html_attrs) do
|
|
21
|
+
concat content_tag(:span, trigger, data: { action: "click->dialog#open" }, class: "contents") if trigger
|
|
21
22
|
concat panel
|
|
22
23
|
end
|
|
23
24
|
end
|
|
@@ -25,17 +26,17 @@ module UI
|
|
|
25
26
|
private
|
|
26
27
|
|
|
27
28
|
def panel
|
|
28
|
-
content_tag(:div, data: {
|
|
29
|
+
content_tag(:div, data: { dialog_target: "panel" }, hidden: true) do
|
|
29
30
|
concat content_tag(:div, nil,
|
|
30
31
|
class: OVERLAY,
|
|
31
|
-
data: { action: "click->
|
|
32
|
+
data: { action: "click->dialog#close" },
|
|
32
33
|
"aria-hidden": "true")
|
|
33
34
|
concat content_tag(:div,
|
|
34
35
|
class: cn(PANEL, @extra_class),
|
|
35
36
|
role: "dialog",
|
|
36
37
|
"aria-modal": "true",
|
|
37
38
|
"aria-label": @title,
|
|
38
|
-
data: { action: "keydown.escape@window->
|
|
39
|
+
data: { action: "keydown.escape@window->dialog#close" }) {
|
|
39
40
|
concat drag_handle
|
|
40
41
|
concat header_area
|
|
41
42
|
concat content_tag(:div, content, class: "px-4 pb-6 text-sm")
|
data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_menu_component.rb.tt
CHANGED
|
@@ -4,20 +4,19 @@ module UI
|
|
|
4
4
|
class DropdownMenuComponent < ApplicationComponent
|
|
5
5
|
renders_one :trigger
|
|
6
6
|
|
|
7
|
-
PANEL = "
|
|
8
|
-
"text-popover-foreground shadow-md"
|
|
7
|
+
PANEL = "#{UI::Styles::POPOVER_PANEL} min-w-[8rem] overflow-x-hidden overflow-y-auto p-1"
|
|
9
8
|
|
|
10
9
|
ALIGN = {
|
|
11
10
|
start: "top-full left-0 mt-1",
|
|
12
11
|
end: "top-full right-0 mt-1",
|
|
13
12
|
}.freeze
|
|
14
13
|
|
|
15
|
-
ITEM = "
|
|
16
|
-
"
|
|
17
|
-
"
|
|
14
|
+
ITEM = "#{UI::Styles::MENU_ITEM} w-full hover:bg-accent hover:text-accent-foreground " \
|
|
15
|
+
"data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 " \
|
|
16
|
+
"dark:data-[variant=destructive]:focus:bg-destructive/20 " \
|
|
18
17
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 " \
|
|
19
18
|
"[&_svg:not([class*='text-'])]:text-muted-foreground"
|
|
20
|
-
SEPARATOR =
|
|
19
|
+
SEPARATOR = UI::Styles::MENU_SEPARATOR
|
|
21
20
|
LABEL_CLS = "px-2 py-1.5 text-sm font-medium"
|
|
22
21
|
|
|
23
22
|
def initialize(align: :start, **html_attrs)
|
|
@@ -48,8 +48,8 @@ module UI
|
|
|
48
48
|
/yandex\.(ru|com)\/maps/i => :yandex_maps
|
|
49
49
|
}.freeze
|
|
50
50
|
|
|
51
|
-
WRAPPER_CLS = "overflow-hidden rounded-md"
|
|
52
|
-
DARK_WRAPPER_CLS = "overflow-hidden rounded-md bg-black"
|
|
51
|
+
WRAPPER_CLS = "overflow-hidden rounded-md #{UI::Styles::BORDER} bg-card shadow-xs"
|
|
52
|
+
DARK_WRAPPER_CLS = "overflow-hidden rounded-md #{UI::Styles::BORDER} bg-black shadow-xs"
|
|
53
53
|
|
|
54
54
|
def self.detect_provider(url)
|
|
55
55
|
DOMAIN_MAP.each { |pattern, provider| return provider if url.to_s.match?(pattern) }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class FigureComponent < ApplicationComponent
|
|
5
|
-
CAPTION = "mt-
|
|
5
|
+
CAPTION = "mt-4 text-sm text-muted-foreground"
|
|
6
6
|
|
|
7
7
|
# caption: text shown in <figcaption> (optional; omit to render none)
|
|
8
8
|
# caption_class: override the figcaption classes
|
|
@@ -2,27 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class FileInputComponent < ApplicationComponent
|
|
5
|
-
|
|
6
|
-
"transition-[color,box-shadow] outline-none " \
|
|
7
|
-
"file:mr-3 file:inline-flex file:h-7 file:cursor-pointer file:border-0 " \
|
|
8
|
-
"file:bg-transparent file:text-sm file:font-medium file:text-foreground " \
|
|
9
|
-
"placeholder:text-muted-foreground " \
|
|
10
|
-
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " \
|
|
11
|
-
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " \
|
|
12
|
-
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " \
|
|
13
|
-
"md:text-sm dark:bg-input/30"
|
|
5
|
+
FILE_EXTRA = "cursor-pointer file:mr-3 file:cursor-pointer"
|
|
14
6
|
|
|
15
7
|
# accept: MIME types or extensions, e.g. "image/*" or ".pdf,.docx"
|
|
16
8
|
# multiple: allow selecting multiple files
|
|
17
9
|
def initialize(accept: nil, multiple: false, **html_attrs)
|
|
18
10
|
@accept = accept
|
|
19
11
|
@multiple = multiple
|
|
20
|
-
|
|
21
|
-
@html_attrs = html_attrs
|
|
12
|
+
extract_html_attrs(**html_attrs)
|
|
22
13
|
end
|
|
23
14
|
|
|
24
15
|
def call
|
|
25
|
-
attrs = { type: "file", class: cn(
|
|
16
|
+
attrs = { type: "file", class: cn(UI::Styles::INPUT, FILE_EXTRA, @extra_class) }
|
|
26
17
|
attrs[:accept] = @accept if @accept
|
|
27
18
|
attrs[:multiple] = true if @multiple
|
|
28
19
|
content_tag(:input, nil, **attrs, **@html_attrs)
|
data/lib/generators/view_primitives/add/templates/floating_label/floating_label_component.rb.tt
CHANGED
|
@@ -8,7 +8,7 @@ module UI
|
|
|
8
8
|
# then rises above when the input is focused or has a value (:not(:placeholder-shown)).
|
|
9
9
|
INPUT_BASE = "peer h-12 w-full min-w-0 rounded-md border border-input bg-transparent px-3 pb-1.5 pt-4 " \
|
|
10
10
|
"text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-transparent " \
|
|
11
|
-
"
|
|
11
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
12
12
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " \
|
|
13
13
|
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " \
|
|
14
14
|
"md:text-sm dark:bg-input/30"
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class FooterComponent < ApplicationComponent
|
|
5
|
-
BASE = "border-t bg-background"
|
|
6
|
-
LINK = "text-sm text-muted-foreground
|
|
5
|
+
BASE = "border-t border-border bg-background"
|
|
6
|
+
LINK = "inline-flex rounded-md px-1 py-0.5 text-sm text-muted-foreground transition-colors " \
|
|
7
|
+
"outline-none hover:text-foreground #{UI::Styles::FOCUS_RING}"
|
|
7
8
|
|
|
8
9
|
# columns: [{ title:, links: [{ label:, href: }] }]
|
|
9
10
|
def initialize(copyright: nil, columns: [], **html_attrs)
|
|
@@ -33,7 +34,7 @@ module UI
|
|
|
33
34
|
|
|
34
35
|
def column(col)
|
|
35
36
|
content_tag(:div) do
|
|
36
|
-
concat content_tag(:h3, col[:title], class: "mb-3 text-sm font-
|
|
37
|
+
concat content_tag(:h3, col[:title], class: "mb-3 text-sm font-medium text-foreground")
|
|
37
38
|
concat content_tag(:ul, class: "space-y-2") {
|
|
38
39
|
safe_join((col[:links] || []).map { |link|
|
|
39
40
|
content_tag(:li) { content_tag(:a, link[:label], href: link[:href], class: LINK) }
|
|
@@ -43,7 +44,7 @@ module UI
|
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def copyright_row
|
|
46
|
-
content_tag(:div, class: "border-t
|
|
47
|
+
content_tag(:div, class: "mt-8 border-t border-border pt-8 text-center") do
|
|
47
48
|
content_tag(:p, @copyright, class: "text-sm text-muted-foreground")
|
|
48
49
|
end
|
|
49
50
|
end
|
|
@@ -8,6 +8,8 @@ module UI
|
|
|
8
8
|
# <%%= ui :input, type: "email", name: "user[email]", id: "user_email" %>
|
|
9
9
|
# <%% end %>
|
|
10
10
|
class FormFieldComponent < ApplicationComponent
|
|
11
|
+
WRAPPER = "group/field flex w-full flex-col gap-3"
|
|
12
|
+
|
|
11
13
|
def initialize(label: nil, hint: nil, error: nil, required: false, **html_attrs)
|
|
12
14
|
@label = label
|
|
13
15
|
@hint = hint
|
|
@@ -18,9 +20,13 @@ module UI
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def call
|
|
21
|
-
content_tag(:div,
|
|
23
|
+
content_tag(:div,
|
|
24
|
+
class: cn(WRAPPER, (@error.present? ? "data-[invalid=true]" : nil), @extra_class),
|
|
25
|
+
"data-slot": "field",
|
|
26
|
+
"data-invalid": @error.present?.to_s,
|
|
27
|
+
**@html_attrs) do
|
|
22
28
|
concat field_label if @label
|
|
23
|
-
concat content
|
|
29
|
+
concat content_tag(:div, content, class: "flex flex-col gap-1.5 leading-snug", "data-slot": "field-content")
|
|
24
30
|
concat hint_tag if @hint && @error.blank?
|
|
25
31
|
concat error_tag if @error.present?
|
|
26
32
|
end
|
|
@@ -31,7 +37,9 @@ module UI
|
|
|
31
37
|
def field_label
|
|
32
38
|
content_tag(:label,
|
|
33
39
|
label_text,
|
|
34
|
-
class: "text-sm font-medium
|
|
40
|
+
class: "flex items-center gap-2 text-sm leading-none font-medium select-none " \
|
|
41
|
+
"group-data-[disabled=true]/field:opacity-50",
|
|
42
|
+
"data-slot": "field-label")
|
|
35
43
|
end
|
|
36
44
|
|
|
37
45
|
def label_text
|
|
@@ -41,11 +49,16 @@ module UI
|
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
def hint_tag
|
|
44
|
-
content_tag(:p, @hint,
|
|
52
|
+
content_tag(:p, @hint,
|
|
53
|
+
class: "text-sm leading-normal font-normal text-muted-foreground",
|
|
54
|
+
"data-slot": "field-description")
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
def error_tag
|
|
48
|
-
content_tag(:p, @error,
|
|
58
|
+
content_tag(:p, @error,
|
|
59
|
+
class: "text-sm font-normal text-destructive",
|
|
60
|
+
role: "alert",
|
|
61
|
+
"data-slot": "field-error")
|
|
49
62
|
end
|
|
50
63
|
end
|
|
51
64
|
end
|
|
@@ -10,15 +10,15 @@ module UI
|
|
|
10
10
|
# g.with_image(src: "/img/b.jpg", alt: "Photo B", caption: "The coast")
|
|
11
11
|
# end
|
|
12
12
|
|
|
13
|
-
GRID_BASE = "grid gap-
|
|
13
|
+
GRID_BASE = "grid gap-4"
|
|
14
14
|
GRID_COLS = {
|
|
15
15
|
1 => "grid-cols-1", 2 => "grid-cols-2", 3 => "grid-cols-3",
|
|
16
16
|
4 => "grid-cols-4", 5 => "grid-cols-5", 6 => "grid-cols-6"
|
|
17
17
|
}.freeze
|
|
18
18
|
|
|
19
|
-
ITEM_CLS = "group relative cursor-zoom-in overflow-hidden rounded-md"
|
|
19
|
+
ITEM_CLS = "group relative cursor-zoom-in overflow-hidden rounded-md #{UI::Styles::BORDER} bg-muted shadow-xs"
|
|
20
20
|
IMG_CLS = "h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
21
|
-
CAP_CLS = "absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/
|
|
21
|
+
CAP_CLS = "absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 to-transparent px-3 py-2 " \
|
|
22
22
|
"text-sm text-white opacity-0 transition-opacity group-hover:opacity-100"
|
|
23
23
|
|
|
24
24
|
renders_many :images, "UI::GalleryComponent::ImageComponent"
|
|
@@ -4,7 +4,7 @@ export default class extends Controller {
|
|
|
4
4
|
open({ params: { src, alt } }) {
|
|
5
5
|
if (this._overlay) return
|
|
6
6
|
const overlay = document.createElement("div")
|
|
7
|
-
overlay.className = "
|
|
7
|
+
overlay.className = "vp-overlay flex items-center justify-center p-4"
|
|
8
8
|
overlay.dataset.galleryOverlay = ""
|
|
9
9
|
|
|
10
10
|
const img = document.createElement("img")
|
|
@@ -4,8 +4,7 @@ module UI
|
|
|
4
4
|
class HoverCardComponent < ApplicationComponent
|
|
5
5
|
renders_one :trigger
|
|
6
6
|
|
|
7
|
-
CARD_BASE = "
|
|
8
|
-
"text-popover-foreground shadow-md " \
|
|
7
|
+
CARD_BASE = "#{UI::Styles::POPOVER_PANEL} w-64 p-4 " \
|
|
9
8
|
"opacity-0 group-hover:opacity-100 pointer-events-none " \
|
|
10
9
|
"transition-opacity duration-200"
|
|
11
10
|
|
|
@@ -24,9 +23,10 @@ module UI
|
|
|
24
23
|
|
|
25
24
|
def call
|
|
26
25
|
content_tag(:span,
|
|
27
|
-
class: cn("relative inline-block
|
|
26
|
+
class: cn("group relative inline-block", @extra_class),
|
|
27
|
+
data: { slot: "hover-card" },
|
|
28
28
|
**@html_attrs) do
|
|
29
|
-
concat trigger if trigger
|
|
29
|
+
concat content_tag(:span, trigger, class: "contents", data: { slot: "hover-card-trigger" }) if trigger
|
|
30
30
|
concat card_panel
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -35,7 +35,8 @@ module UI
|
|
|
35
35
|
|
|
36
36
|
def card_panel
|
|
37
37
|
content_tag(:div,
|
|
38
|
-
class: cn(CARD_BASE, POSITIONS.fetch(@side, POSITIONS[:bottom]))
|
|
38
|
+
class: cn(CARD_BASE, POSITIONS.fetch(@side, POSITIONS[:bottom])),
|
|
39
|
+
data: { slot: "hover-card-content" }) do
|
|
39
40
|
content
|
|
40
41
|
end
|
|
41
42
|
end
|
|
@@ -27,8 +27,10 @@ module UI
|
|
|
27
27
|
|
|
28
28
|
def call
|
|
29
29
|
if @aspect
|
|
30
|
-
content_tag(:div,
|
|
31
|
-
|
|
30
|
+
content_tag(:div,
|
|
31
|
+
style: "aspect-ratio: #{@aspect}",
|
|
32
|
+
class: cn("w-full overflow-hidden rounded-md #{UI::Styles::BORDER} shadow-xs", @extra_class)) do
|
|
33
|
+
iframe_tag(wrapped: true)
|
|
32
34
|
end
|
|
33
35
|
else
|
|
34
36
|
iframe_tag
|
|
@@ -37,12 +39,12 @@ module UI
|
|
|
37
39
|
|
|
38
40
|
private
|
|
39
41
|
|
|
40
|
-
def iframe_tag
|
|
42
|
+
def iframe_tag(wrapped: false)
|
|
41
43
|
attrs = {
|
|
42
44
|
src: @src,
|
|
43
45
|
title: @title,
|
|
44
46
|
loading: @loading,
|
|
45
|
-
class: cn(BASE, (
|
|
47
|
+
class: cn(BASE, (wrapped ? "h-full" : @extra_class))
|
|
46
48
|
}
|
|
47
49
|
attrs[:sandbox] = sandbox_value if @sandbox != false
|
|
48
50
|
attrs[:width] = @width if @width
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class IndicatorComponent < ApplicationComponent
|
|
5
|
-
DOT_BASE = "absolute flex items-center justify-center rounded-full text-[10px] font-medium leading-none"
|
|
5
|
+
DOT_BASE = "absolute flex items-center justify-center rounded-full text-[10px] font-medium leading-none " \
|
|
6
|
+
"ring-2 ring-background"
|
|
6
7
|
|
|
7
8
|
VARIANTS = {
|
|
8
9
|
default: "bg-primary text-primary-foreground",
|
|
9
|
-
destructive: "bg-destructive text-white",
|
|
10
|
-
success: "bg-
|
|
11
|
-
warning: "bg-
|
|
10
|
+
destructive: "bg-destructive text-white dark:bg-destructive/60",
|
|
11
|
+
success: "bg-chart-2 text-white",
|
|
12
|
+
warning: "bg-chart-4 text-foreground"
|
|
12
13
|
}.freeze
|
|
13
14
|
|
|
14
15
|
POSITIONS = {
|
|
@@ -2,26 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class InputComponent < ApplicationComponent
|
|
5
|
-
BASE = "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs " \
|
|
6
|
-
"transition-[color,box-shadow] outline-none " \
|
|
7
|
-
"selection:bg-primary selection:text-primary-foreground " \
|
|
8
|
-
"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground " \
|
|
9
|
-
"placeholder:text-muted-foreground " \
|
|
10
|
-
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " \
|
|
11
|
-
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " \
|
|
12
|
-
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " \
|
|
13
|
-
"md:text-sm dark:bg-input/30"
|
|
14
|
-
|
|
15
5
|
def initialize(type: "text", **html_attrs)
|
|
16
6
|
@type = type
|
|
17
|
-
|
|
18
|
-
@html_attrs = html_attrs
|
|
7
|
+
extract_html_attrs(**html_attrs)
|
|
19
8
|
end
|
|
20
9
|
|
|
21
10
|
def call
|
|
22
11
|
content_tag(:input, nil,
|
|
23
12
|
type: @type,
|
|
24
|
-
class: cn(
|
|
13
|
+
class: cn(UI::Styles::INPUT, @extra_class),
|
|
25
14
|
**@html_attrs)
|
|
26
15
|
end
|
|
27
16
|
end
|