view_primitives 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/CHANGELOG.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +198 -0
- data/lib/generators/view_primitives/add/add_generator.rb +110 -0
- data/lib/generators/view_primitives/add/templates/accordion/accordion_component.html.erb +10 -0
- data/lib/generators/view_primitives/add/templates/accordion/accordion_component.rb.tt +22 -0
- data/lib/generators/view_primitives/add/templates/accordion/accordion_controller.js +15 -0
- data/lib/generators/view_primitives/add/templates/accordion/accordion_item_component.rb.tt +47 -0
- data/lib/generators/view_primitives/add/templates/alert/alert_component.rb.tt +62 -0
- data/lib/generators/view_primitives/add/templates/alert_dialog/alert_dialog_component.rb.tt +55 -0
- data/lib/generators/view_primitives/add/templates/aspect_ratio/aspect_ratio_component.rb.tt +18 -0
- data/lib/generators/view_primitives/add/templates/audio/audio_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/avatar/avatar_component.rb.tt +37 -0
- data/lib/generators/view_primitives/add/templates/badge/badge_component.rb.tt +35 -0
- data/lib/generators/view_primitives/add/templates/banner/banner_component.rb.tt +29 -0
- data/lib/generators/view_primitives/add/templates/bottom_nav/bottom_nav_component.rb.tt +38 -0
- data/lib/generators/view_primitives/add/templates/breadcrumb/breadcrumb_component.rb.tt +37 -0
- data/lib/generators/view_primitives/add/templates/button/button_component.rb.tt +61 -0
- data/lib/generators/view_primitives/add/templates/button_group/button_group_component.rb.tt +23 -0
- data/lib/generators/view_primitives/add/templates/calendar/calendar_component.rb.tt +121 -0
- data/lib/generators/view_primitives/add/templates/calendar/calendar_controller.js +86 -0
- data/lib/generators/view_primitives/add/templates/card/card_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/card/card_content_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/card/card_description_component.rb.tt +17 -0
- data/lib/generators/view_primitives/add/templates/card/card_footer_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/card/card_header_component.rb.tt +17 -0
- data/lib/generators/view_primitives/add/templates/card/card_title_component.rb.tt +17 -0
- data/lib/generators/view_primitives/add/templates/carousel/carousel_component.rb.tt +102 -0
- data/lib/generators/view_primitives/add/templates/carousel/carousel_controller.js +48 -0
- data/lib/generators/view_primitives/add/templates/chart/chart_component.rb.tt +63 -0
- data/lib/generators/view_primitives/add/templates/chart/chart_controller.js +29 -0
- data/lib/generators/view_primitives/add/templates/chat_bubble/chat_bubble_component.rb.tt +53 -0
- data/lib/generators/view_primitives/add/templates/checkbox/checkbox_component.rb.tt +50 -0
- data/lib/generators/view_primitives/add/templates/collapsible/collapsible_component.rb.tt +31 -0
- data/lib/generators/view_primitives/add/templates/combobox/combobox_component.rb.tt +87 -0
- data/lib/generators/view_primitives/add/templates/combobox/combobox_controller.js +38 -0
- data/lib/generators/view_primitives/add/templates/command/command_component.rb.tt +85 -0
- 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 +47 -0
- data/lib/generators/view_primitives/add/templates/context_menu/context_menu_controller.js +20 -0
- data/lib/generators/view_primitives/add/templates/data_table/data_table_component.rb.tt +163 -0
- data/lib/generators/view_primitives/add/templates/data_table/data_table_controller.js +115 -0
- data/lib/generators/view_primitives/add/templates/date_picker/date_picker_component.rb.tt +92 -0
- data/lib/generators/view_primitives/add/templates/date_picker/date_picker_controller.js +48 -0
- data/lib/generators/view_primitives/add/templates/device_mockup/device_mockup_component.rb.tt +65 -0
- data/lib/generators/view_primitives/add/templates/dialog/dialog_component.rb.tt +71 -0
- data/lib/generators/view_primitives/add/templates/dialog/dialog_controller.js +15 -0
- data/lib/generators/view_primitives/add/templates/drawer/drawer_component.rb.tt +62 -0
- data/lib/generators/view_primitives/add/templates/drawer/drawer_controller.js +15 -0
- data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_controller.js +17 -0
- data/lib/generators/view_primitives/add/templates/dropdown_menu/dropdown_menu_component.rb.tt +53 -0
- data/lib/generators/view_primitives/add/templates/embed/embed_component.rb.tt +271 -0
- data/lib/generators/view_primitives/add/templates/embed/embed_controller.js +43 -0
- data/lib/generators/view_primitives/add/templates/figure/figure_component.rb.tt +24 -0
- data/lib/generators/view_primitives/add/templates/file_input/file_input_component.rb.tt +31 -0
- data/lib/generators/view_primitives/add/templates/floating_label/floating_label_component.rb.tt +54 -0
- data/lib/generators/view_primitives/add/templates/footer/footer_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/form_field/form_field_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/gallery/gallery_component.rb.tt +83 -0
- data/lib/generators/view_primitives/add/templates/gallery/gallery_controller.js +28 -0
- data/lib/generators/view_primitives/add/templates/hover_card/hover_card_component.rb.tt +43 -0
- data/lib/generators/view_primitives/add/templates/iframe/iframe_component.rb.tt +59 -0
- data/lib/generators/view_primitives/add/templates/image/image_component.rb.tt +38 -0
- data/lib/generators/view_primitives/add/templates/indicator/indicator_component.rb.tt +46 -0
- data/lib/generators/view_primitives/add/templates/input/input_component.rb.tt +28 -0
- data/lib/generators/view_primitives/add/templates/input_otp/input_otp_component.rb.tt +65 -0
- data/lib/generators/view_primitives/add/templates/input_otp/input_otp_controller.js +39 -0
- data/lib/generators/view_primitives/add/templates/kbd/kbd_component.rb.tt +21 -0
- data/lib/generators/view_primitives/add/templates/label/label_component.rb.tt +23 -0
- data/lib/generators/view_primitives/add/templates/list_group/list_group_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/list_group/list_group_item_component.rb.tt +31 -0
- data/lib/generators/view_primitives/add/templates/map_area/map_area_component.rb.tt +72 -0
- data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_component.rb.tt +130 -0
- data/lib/generators/view_primitives/add/templates/mega_menu/mega_menu_controller.js +23 -0
- data/lib/generators/view_primitives/add/templates/menubar/menubar_component.rb.tt +33 -0
- data/lib/generators/view_primitives/add/templates/menubar/menubar_controller.js +34 -0
- data/lib/generators/view_primitives/add/templates/menubar/menubar_menu_component.rb.tt +34 -0
- data/lib/generators/view_primitives/add/templates/navbar/navbar_component.rb.tt +90 -0
- data/lib/generators/view_primitives/add/templates/navbar/navbar_controller.js +11 -0
- data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_component.rb.tt +132 -0
- data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_controller.js +25 -0
- data/lib/generators/view_primitives/add/templates/number_input/number_input_component.rb.tt +34 -0
- data/lib/generators/view_primitives/add/templates/pagination/pagination_component.rb.tt +97 -0
- data/lib/generators/view_primitives/add/templates/picture/picture_component.rb.tt +63 -0
- data/lib/generators/view_primitives/add/templates/popover/popover_component.rb.tt +56 -0
- data/lib/generators/view_primitives/add/templates/popover/popover_controller.js +17 -0
- data/lib/generators/view_primitives/add/templates/progress/progress_component.rb.tt +28 -0
- data/lib/generators/view_primitives/add/templates/qr_code/qr_code_component.rb.tt +39 -0
- data/lib/generators/view_primitives/add/templates/radio_group/radio_group_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/range/range_component.rb.tt +40 -0
- data/lib/generators/view_primitives/add/templates/rating/rating_component.rb.tt +42 -0
- data/lib/generators/view_primitives/add/templates/rating_input/rating_controller.js +47 -0
- data/lib/generators/view_primitives/add/templates/rating_input/rating_input_component.rb.tt +79 -0
- data/lib/generators/view_primitives/add/templates/resizable/resizable_component.rb.tt +91 -0
- data/lib/generators/view_primitives/add/templates/resizable/resizable_controller.js +38 -0
- data/lib/generators/view_primitives/add/templates/scroll_area/scroll_area_component.rb.tt +41 -0
- data/lib/generators/view_primitives/add/templates/search_input/search_input_component.rb.tt +50 -0
- data/lib/generators/view_primitives/add/templates/select/select_component.rb.tt +45 -0
- data/lib/generators/view_primitives/add/templates/separator/separator_component.rb.tt +25 -0
- data/lib/generators/view_primitives/add/templates/sheet/sheet_component.rb.tt +78 -0
- data/lib/generators/view_primitives/add/templates/sheet/sheet_controller.js +15 -0
- data/lib/generators/view_primitives/add/templates/sidebar/sidebar_component.rb.tt +169 -0
- data/lib/generators/view_primitives/add/templates/sidebar/sidebar_controller.js +11 -0
- data/lib/generators/view_primitives/add/templates/skeleton/skeleton_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_component.rb.tt +111 -0
- data/lib/generators/view_primitives/add/templates/speed_dial/speed_dial_controller.js +22 -0
- data/lib/generators/view_primitives/add/templates/spinner/spinner_component.rb.tt +27 -0
- data/lib/generators/view_primitives/add/templates/stepper/stepper_component.rb.tt +99 -0
- data/lib/generators/view_primitives/add/templates/switch/switch_component.rb.tt +51 -0
- data/lib/generators/view_primitives/add/templates/tabs/tabs_component.html.erb +50 -0
- data/lib/generators/view_primitives/add/templates/tabs/tabs_component.rb.tt +16 -0
- data/lib/generators/view_primitives/add/templates/tabs/tabs_controller.js +26 -0
- data/lib/generators/view_primitives/add/templates/tabs/tabs_item_component.rb.tt +15 -0
- data/lib/generators/view_primitives/add/templates/textarea/textarea_component.rb.tt +24 -0
- data/lib/generators/view_primitives/add/templates/timeline/timeline_component.rb.tt +78 -0
- data/lib/generators/view_primitives/add/templates/timepicker/timepicker_component.rb.tt +140 -0
- data/lib/generators/view_primitives/add/templates/timepicker/timepicker_controller.js +92 -0
- data/lib/generators/view_primitives/add/templates/toaster/toaster_component.rb.tt +152 -0
- data/lib/generators/view_primitives/add/templates/toaster/toaster_controller.js +88 -0
- data/lib/generators/view_primitives/add/templates/toggle/toggle_component.rb.tt +41 -0
- data/lib/generators/view_primitives/add/templates/toggle/toggle_controller.js +12 -0
- data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_component.rb.tt +32 -0
- data/lib/generators/view_primitives/add/templates/toggle_group/toggle_group_controller.js +38 -0
- data/lib/generators/view_primitives/add/templates/tooltip/tooltip_component.rb.tt +42 -0
- data/lib/generators/view_primitives/add/templates/video/video_component.rb.tt +92 -0
- data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_component.rb.tt +88 -0
- data/lib/generators/view_primitives/add/templates/wysiwyg/wysiwyg_controller.js +40 -0
- data/lib/generators/view_primitives/components.rb +62 -0
- data/lib/generators/view_primitives/detector.rb +43 -0
- data/lib/generators/view_primitives/install/install_generator.rb +65 -0
- data/lib/generators/view_primitives/install/templates/application_component.rb.tt +5 -0
- data/lib/generators/view_primitives/install/templates/view_primitives.css +67 -0
- data/lib/generators/view_primitives/list/list_generator.rb +25 -0
- data/lib/view_primitives/class_helper.rb +11 -0
- data/lib/view_primitives/component_helper.rb +20 -0
- data/lib/view_primitives/railtie.rb +21 -0
- data/lib/view_primitives/version.rb +5 -0
- data/lib/view_primitives.rb +12 -0
- metadata +267 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["panel"]
|
|
5
|
+
|
|
6
|
+
open() {
|
|
7
|
+
this.panelTarget.hidden = false
|
|
8
|
+
document.body.style.overflow = "hidden"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
close() {
|
|
12
|
+
this.panelTarget.hidden = true
|
|
13
|
+
document.body.style.overflow = ""
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class SidebarComponent < ApplicationComponent
|
|
5
|
+
# Collapsible application sidebar with nav groups.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ui :sidebar do |s|
|
|
9
|
+
# s.with_group(label: "Main") do |g|
|
|
10
|
+
# g.with_item(label: "Dashboard", href: "/", icon: :home, active: true)
|
|
11
|
+
# g.with_item(label: "Settings", href: "/settings", icon: :settings)
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
|
|
15
|
+
RAIL_CLS = "group peer fixed inset-y-0 left-0 z-30 flex h-full flex-col " \
|
|
16
|
+
"border-r border-border bg-background transition-[width] duration-300 " \
|
|
17
|
+
"data-[collapsed=true]:w-16 data-[collapsed=false]:w-64"
|
|
18
|
+
|
|
19
|
+
HEADER_CLS = "flex h-14 items-center justify-between border-b border-border px-4"
|
|
20
|
+
|
|
21
|
+
TOGGLE_CLS = "inline-flex size-8 items-center justify-center rounded-md " \
|
|
22
|
+
"text-muted-foreground hover:bg-accent hover:text-accent-foreground " \
|
|
23
|
+
"focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none transition"
|
|
24
|
+
|
|
25
|
+
NAV_CLS = "flex-1 overflow-y-auto px-2 py-3"
|
|
26
|
+
|
|
27
|
+
GROUP_LABEL = "mb-1 px-3 text-xs font-medium uppercase tracking-wide text-muted-foreground " \
|
|
28
|
+
"transition-opacity group-data-[collapsed=true]:opacity-0 group-data-[collapsed=true]:h-0 " \
|
|
29
|
+
"group-data-[collapsed=true]:overflow-hidden group-data-[collapsed=true]:mb-0"
|
|
30
|
+
|
|
31
|
+
ITEM_CLS = "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors " \
|
|
32
|
+
"overflow-hidden " \
|
|
33
|
+
"group-data-[collapsed=true]:justify-center group-data-[collapsed=true]:gap-0 " \
|
|
34
|
+
"hover:bg-accent hover:text-accent-foreground " \
|
|
35
|
+
"aria-[current]:bg-accent aria-[current]:text-foreground aria-[current]:font-semibold " \
|
|
36
|
+
"focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none"
|
|
37
|
+
|
|
38
|
+
ITEM_LABEL = "transition-[opacity,width] group-data-[collapsed=true]:w-0 " \
|
|
39
|
+
"group-data-[collapsed=true]:opacity-0 group-data-[collapsed=true]:overflow-hidden " \
|
|
40
|
+
"whitespace-nowrap"
|
|
41
|
+
|
|
42
|
+
renders_many :groups, "UI::SidebarComponent::GroupComponent"
|
|
43
|
+
renders_many :items, "UI::SidebarComponent::ItemComponent"
|
|
44
|
+
|
|
45
|
+
# brand: text shown in the header
|
|
46
|
+
# collapsed: initial collapsed state (default: false)
|
|
47
|
+
def initialize(brand: nil, collapsed: false, **html_attrs)
|
|
48
|
+
@brand = brand
|
|
49
|
+
@collapsed = collapsed
|
|
50
|
+
@extra_class = html_attrs.delete(:class)
|
|
51
|
+
@html_attrs = html_attrs
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call
|
|
55
|
+
content_tag(:aside,
|
|
56
|
+
class: cn(RAIL_CLS, @extra_class),
|
|
57
|
+
"data-collapsed": @collapsed.to_s,
|
|
58
|
+
data: { controller: "sidebar" },
|
|
59
|
+
**@html_attrs) do
|
|
60
|
+
concat header
|
|
61
|
+
concat nav_body
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def header
|
|
68
|
+
content_tag(:div, class: HEADER_CLS) do
|
|
69
|
+
concat content_tag(:span, @brand,
|
|
70
|
+
class: "truncate font-semibold group-data-[collapsed=true]:hidden") if @brand
|
|
71
|
+
concat toggle_btn
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def toggle_btn
|
|
76
|
+
content_tag(:button, type: "button",
|
|
77
|
+
class: TOGGLE_CLS,
|
|
78
|
+
"aria-label": "Toggle sidebar",
|
|
79
|
+
data: { action: "click->sidebar#toggle" }) { chevron_icon }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def nav_body
|
|
83
|
+
content_tag(:nav, class: NAV_CLS) do
|
|
84
|
+
concat safe_join(groups) if groups.any?
|
|
85
|
+
concat content_tag(:div, safe_join(items), class: "space-y-0.5") if items.any?
|
|
86
|
+
concat content if content?
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def chevron_icon
|
|
91
|
+
content_tag(:svg,
|
|
92
|
+
content_tag(:path, nil, d: "m15 18-6-6 6-6", "stroke-linecap": "round", "stroke-linejoin": "round"),
|
|
93
|
+
xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24",
|
|
94
|
+
fill: "none", stroke: "currentColor", "stroke-width": "2",
|
|
95
|
+
class: "size-4 transition-transform group-data-[collapsed=true]:rotate-180",
|
|
96
|
+
"aria-hidden": "true")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class GroupComponent < ApplicationComponent
|
|
100
|
+
renders_many :items, "UI::SidebarComponent::ItemComponent"
|
|
101
|
+
|
|
102
|
+
def initialize(label: nil, **html_attrs)
|
|
103
|
+
@label = label
|
|
104
|
+
@html_attrs = html_attrs
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def call
|
|
108
|
+
content_tag(:div, class: "mb-4", **@html_attrs) do
|
|
109
|
+
concat content_tag(:p, @label, class: SidebarComponent::GROUP_LABEL) if @label
|
|
110
|
+
concat content_tag(:div, safe_join(items), class: "space-y-0.5")
|
|
111
|
+
concat content if content?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class ItemComponent < ApplicationComponent
|
|
117
|
+
ICONS = {
|
|
118
|
+
home: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z M9 22V12h6v10",
|
|
119
|
+
dashboard: "M3 3h7v9H3z M14 3h7v5h-7z M14 12h7v9h-7z M3 16h7v6H3z",
|
|
120
|
+
folder: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z",
|
|
121
|
+
tasks: "M9 11l3 3L22 4 M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11",
|
|
122
|
+
settings: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z",
|
|
123
|
+
users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75",
|
|
124
|
+
chart: "M18 20V10 M12 20V4 M6 20v-6",
|
|
125
|
+
mail: "M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z M22 6l-10 7L2 6",
|
|
126
|
+
bell: "M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9 M13.73 21a2 2 0 0 1-3.46 0",
|
|
127
|
+
credit_card: "M1 4h22v16H1z M1 10h22",
|
|
128
|
+
logout: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4 M16 17l5-5-5-5 M21 12H9",
|
|
129
|
+
}.freeze
|
|
130
|
+
|
|
131
|
+
def initialize(label:, href: "#", active: false, icon: nil, **html_attrs)
|
|
132
|
+
@label = label
|
|
133
|
+
@href = href
|
|
134
|
+
@active = active
|
|
135
|
+
@icon = icon
|
|
136
|
+
@html_attrs = html_attrs
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def call
|
|
140
|
+
content_tag(:a,
|
|
141
|
+
href: @href,
|
|
142
|
+
class: SidebarComponent::ITEM_CLS,
|
|
143
|
+
"aria-current": (@active ? "page" : nil),
|
|
144
|
+
**@html_attrs) do
|
|
145
|
+
concat icon_or_fallback
|
|
146
|
+
concat content_tag(:span, @label, class: SidebarComponent::ITEM_LABEL)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def icon_or_fallback
|
|
153
|
+
path = @icon && ICONS[@icon.to_sym]
|
|
154
|
+
if path
|
|
155
|
+
content_tag(:svg,
|
|
156
|
+
content_tag(:path, nil, d: path, "stroke-linecap": "round", "stroke-linejoin": "round"),
|
|
157
|
+
xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24",
|
|
158
|
+
fill: "none", stroke: "currentColor", "stroke-width": "2",
|
|
159
|
+
class: "size-4 shrink-0",
|
|
160
|
+
"aria-hidden": "true")
|
|
161
|
+
else
|
|
162
|
+
content_tag(:span, @label[0],
|
|
163
|
+
class: "hidden size-5 shrink-0 items-center justify-center rounded text-xs font-semibold group-data-[collapsed=true]:flex",
|
|
164
|
+
"aria-hidden": "true")
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
toggle() {
|
|
5
|
+
const collapsed = this.element.dataset.collapsed === "true"
|
|
6
|
+
this.element.dataset.collapsed = String(!collapsed)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
open() { this.element.dataset.collapsed = "false" }
|
|
10
|
+
close() { this.element.dataset.collapsed = "true" }
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class SkeletonComponent < ApplicationComponent
|
|
5
|
+
BASE = "bg-accent animate-pulse rounded-md"
|
|
6
|
+
|
|
7
|
+
def initialize(**html_attrs)
|
|
8
|
+
@extra_class = html_attrs.delete(:class)
|
|
9
|
+
@html_attrs = html_attrs
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
content_tag(:div, nil, class: cn(BASE, @extra_class), **@html_attrs)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class SpeedDialComponent < ApplicationComponent
|
|
5
|
+
# Floating action button that expands into a stack of sub-action buttons.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ui :speed_dial, icon: :plus do |dial|
|
|
9
|
+
# dial.with_action(label: "New document", icon: :file, href: "/docs/new")
|
|
10
|
+
# dial.with_action(label: "Upload", icon: :upload, data: { action: "..." })
|
|
11
|
+
# end
|
|
12
|
+
|
|
13
|
+
FAB_CLS = "relative z-50 inline-flex size-14 items-center justify-center rounded-full " \
|
|
14
|
+
"bg-primary text-primary-foreground shadow-lg transition-transform " \
|
|
15
|
+
"hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-[3px] " \
|
|
16
|
+
"focus-visible:ring-ring/50 active:scale-95"
|
|
17
|
+
|
|
18
|
+
PANEL_CLS = "absolute bottom-16 right-0 flex flex-col-reverse items-end gap-2"
|
|
19
|
+
|
|
20
|
+
ACTION_CLS = "flex items-center gap-2 rounded-full bg-background px-4 py-2 text-sm font-medium " \
|
|
21
|
+
"shadow-md border border-border transition-all " \
|
|
22
|
+
"hover:bg-accent hover:text-accent-foreground " \
|
|
23
|
+
"focus-visible:ring-[3px] focus-visible:ring-ring/50 outline-none " \
|
|
24
|
+
"whitespace-nowrap"
|
|
25
|
+
|
|
26
|
+
PLUS_PATH = "M12 5v14M5 12h14"
|
|
27
|
+
|
|
28
|
+
renders_many :actions, "UI::SpeedDialComponent::ActionComponent"
|
|
29
|
+
|
|
30
|
+
# position: :bottom_right (default) | :bottom_left | :bottom_center
|
|
31
|
+
def initialize(position: :bottom_right, **html_attrs)
|
|
32
|
+
@position = position.to_sym
|
|
33
|
+
@extra_class = html_attrs.delete(:class)
|
|
34
|
+
@html_attrs = html_attrs
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call
|
|
38
|
+
position_cls = {
|
|
39
|
+
bottom_right: "fixed bottom-6 right-6",
|
|
40
|
+
bottom_left: "fixed bottom-6 left-6",
|
|
41
|
+
bottom_center: "fixed bottom-6 left-1/2 -translate-x-1/2"
|
|
42
|
+
}.fetch(@position, "fixed bottom-6 right-6")
|
|
43
|
+
|
|
44
|
+
content_tag(:div,
|
|
45
|
+
class: cn("relative", position_cls, @extra_class),
|
|
46
|
+
data: {
|
|
47
|
+
controller: "speed-dial",
|
|
48
|
+
action: "click@document->speed-dial#closeOnClickOutside"
|
|
49
|
+
},
|
|
50
|
+
**@html_attrs) do
|
|
51
|
+
concat action_panel
|
|
52
|
+
concat fab_button
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def fab_button
|
|
59
|
+
content_tag(:button,
|
|
60
|
+
type: "button",
|
|
61
|
+
class: FAB_CLS,
|
|
62
|
+
"aria-expanded": "false",
|
|
63
|
+
"aria-label": "Open actions",
|
|
64
|
+
data: {
|
|
65
|
+
speed_dial_target: "fab",
|
|
66
|
+
action: "click->speed-dial#toggle"
|
|
67
|
+
}) do
|
|
68
|
+
plus_icon
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def action_panel
|
|
73
|
+
content_tag(:div,
|
|
74
|
+
class: PANEL_CLS,
|
|
75
|
+
hidden: true,
|
|
76
|
+
data: { speed_dial_target: "panel" }) do
|
|
77
|
+
safe_join(actions)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def plus_icon
|
|
82
|
+
content_tag(:svg,
|
|
83
|
+
content_tag(:path, nil, d: PLUS_PATH, "stroke-linecap": "round", "stroke-linejoin": "round"),
|
|
84
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
85
|
+
viewBox: "0 0 24 24",
|
|
86
|
+
fill: "none",
|
|
87
|
+
stroke: "currentColor",
|
|
88
|
+
"stroke-width": "2",
|
|
89
|
+
class: "size-6 transition-transform duration-200",
|
|
90
|
+
"aria-hidden": "true",
|
|
91
|
+
data: { speed_dial_target: "icon" })
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class ActionComponent < ApplicationComponent
|
|
95
|
+
def initialize(label:, href: nil, icon: nil, **html_attrs)
|
|
96
|
+
@label = label
|
|
97
|
+
@href = href
|
|
98
|
+
@icon = icon
|
|
99
|
+
@html_attrs = html_attrs
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def call
|
|
103
|
+
tag_name = @href ? :a : :button
|
|
104
|
+
attrs = { class: SpeedDialComponent::ACTION_CLS, **@html_attrs }
|
|
105
|
+
attrs[:href] = @href if @href
|
|
106
|
+
attrs[:type] = "button" if tag_name == :button
|
|
107
|
+
content_tag(tag_name, @label, **attrs)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["fab", "panel", "icon"]
|
|
5
|
+
|
|
6
|
+
toggle() {
|
|
7
|
+
const open = this.panelTarget.hidden
|
|
8
|
+
this._setOpen(open)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
closeOnClickOutside(event) {
|
|
12
|
+
if (!this.element.contains(event.target)) this._setOpen(false)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_setOpen(open) {
|
|
16
|
+
this.panelTarget.hidden = !open
|
|
17
|
+
this.fabTarget.setAttribute("aria-expanded", String(open))
|
|
18
|
+
if (this.hasIconTarget) {
|
|
19
|
+
this.iconTarget.style.transform = open ? "rotate(45deg)" : ""
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class SpinnerComponent < ApplicationComponent
|
|
5
|
+
BASE = "inline-block animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
6
|
+
|
|
7
|
+
SIZES = {
|
|
8
|
+
sm: "size-4",
|
|
9
|
+
default: "size-6",
|
|
10
|
+
lg: "size-10"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(size: :default, **html_attrs)
|
|
14
|
+
@size = size.to_sym
|
|
15
|
+
@extra_class = html_attrs.delete(:class)
|
|
16
|
+
@html_attrs = html_attrs
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
content_tag(:span,
|
|
21
|
+
content_tag(:span, "Loading...", class: "sr-only"),
|
|
22
|
+
class: cn(BASE, SIZES.fetch(@size, SIZES[:default]), @extra_class),
|
|
23
|
+
role: "status",
|
|
24
|
+
**@html_attrs)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class StepperComponent < ApplicationComponent
|
|
5
|
+
# steps: [{ label:, description: (optional), status: :complete | :current | :pending }]
|
|
6
|
+
def initialize(steps:, orientation: :horizontal, **html_attrs)
|
|
7
|
+
@steps = steps
|
|
8
|
+
@orientation = orientation.to_sym
|
|
9
|
+
@extra_class = html_attrs.delete(:class)
|
|
10
|
+
@html_attrs = html_attrs
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
wrapper_class = @orientation == :vertical \
|
|
15
|
+
? "flex flex-col gap-0" \
|
|
16
|
+
: "flex items-start gap-0"
|
|
17
|
+
|
|
18
|
+
content_tag(:ol,
|
|
19
|
+
class: cn(wrapper_class, @extra_class),
|
|
20
|
+
"aria-label": "Progress",
|
|
21
|
+
**@html_attrs) do
|
|
22
|
+
safe_join(@steps.each_with_index.map { |step, i| step_item(step, i) })
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def step_item(step, index)
|
|
29
|
+
is_last = index == @steps.size - 1
|
|
30
|
+
status = step.fetch(:status, :pending).to_sym
|
|
31
|
+
|
|
32
|
+
if @orientation == :vertical
|
|
33
|
+
vertical_item(step, status, is_last)
|
|
34
|
+
else
|
|
35
|
+
horizontal_item(step, status, is_last)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def horizontal_item(step, status, is_last)
|
|
40
|
+
content_tag(:li, class: "flex items-center #{is_last ? '' : 'flex-1'}") do
|
|
41
|
+
concat step_circle(status, step[:label])
|
|
42
|
+
concat content_tag(:p, step[:label], class: cn("ml-2 text-sm font-medium whitespace-nowrap", label_color(status))) unless step[:label].nil?
|
|
43
|
+
concat connector(:horizontal, status) unless is_last
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def vertical_item(step, status, is_last)
|
|
48
|
+
content_tag(:li, class: "relative flex gap-4") do
|
|
49
|
+
concat content_tag(:div, class: "flex flex-col items-center") {
|
|
50
|
+
concat step_circle(status, step[:label])
|
|
51
|
+
concat connector(:vertical, status) unless is_last
|
|
52
|
+
}
|
|
53
|
+
concat content_tag(:div, class: "pb-6 pt-0.5 min-w-0") {
|
|
54
|
+
concat content_tag(:p, step[:label], class: cn("text-sm font-medium", label_color(status)))
|
|
55
|
+
concat content_tag(:p, step[:description], class: "mt-0.5 text-xs text-muted-foreground") if step[:description]
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def step_circle(status, label)
|
|
61
|
+
base = "flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold border-2"
|
|
62
|
+
case status
|
|
63
|
+
when :complete
|
|
64
|
+
content_tag(:span, check_svg,
|
|
65
|
+
class: cn(base, "border-primary bg-primary text-primary-foreground"),
|
|
66
|
+
"aria-label": "Completed")
|
|
67
|
+
when :current
|
|
68
|
+
content_tag(:span, "●",
|
|
69
|
+
class: cn(base, "border-primary text-primary"),
|
|
70
|
+
"aria-current": "step")
|
|
71
|
+
else
|
|
72
|
+
content_tag(:span, "○",
|
|
73
|
+
class: cn(base, "border-muted-foreground text-muted-foreground"),
|
|
74
|
+
"aria-label": "Pending")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def connector(direction, status)
|
|
79
|
+
filled = status == :complete
|
|
80
|
+
if direction == :horizontal
|
|
81
|
+
content_tag(:div, nil, class: cn("h-0.5 flex-1 mx-2", filled ? "bg-primary" : "bg-border"))
|
|
82
|
+
else
|
|
83
|
+
content_tag(:div, nil, class: cn("w-0.5 flex-1 my-1", filled ? "bg-primary" : "bg-border"))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def label_color(status)
|
|
88
|
+
case status
|
|
89
|
+
when :complete then "text-foreground"
|
|
90
|
+
when :current then "text-primary"
|
|
91
|
+
else "text-muted-foreground"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def check_svg
|
|
96
|
+
raw('<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>')
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class SwitchComponent < ApplicationComponent
|
|
5
|
+
# sr-only input, track label, and thumb are all siblings inside a relative wrapper
|
|
6
|
+
# so every element can use peer-checked: / peer-focus-visible: / peer-disabled: directly.
|
|
7
|
+
WRAPPER = "relative inline-flex h-[1.15rem] w-8 shrink-0"
|
|
8
|
+
TRACK = "absolute inset-0 cursor-pointer rounded-full border border-transparent shadow-xs " \
|
|
9
|
+
"transition-all bg-input peer-checked:bg-primary dark:bg-input/80 " \
|
|
10
|
+
"peer-focus-visible:border-ring peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50 " \
|
|
11
|
+
"peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
|
|
12
|
+
THUMB = "pointer-events-none absolute inset-y-0 left-[1px] my-auto z-10 block size-4 rounded-full " \
|
|
13
|
+
"bg-background ring-0 transition-transform " \
|
|
14
|
+
"translate-x-0 peer-checked:translate-x-[calc(100%-2px)]"
|
|
15
|
+
|
|
16
|
+
def initialize(label: nil, checked: false, **html_attrs)
|
|
17
|
+
@label = label
|
|
18
|
+
@checked = checked
|
|
19
|
+
@id = html_attrs[:id] || html_attrs[:name]&.gsub(/[\[\]]+/, "_") || "switch_#{object_id}"
|
|
20
|
+
@extra_class = html_attrs.delete(:class)
|
|
21
|
+
@html_attrs = html_attrs
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
content_tag(:div, class: cn("inline-flex items-center gap-2", @extra_class)) do
|
|
26
|
+
concat switch_widget
|
|
27
|
+
concat text_label if @label
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def switch_widget
|
|
34
|
+
content_tag(:div, class: WRAPPER) do
|
|
35
|
+
input_attrs = { type: "checkbox", id: @id, class: "peer sr-only", role: "switch",
|
|
36
|
+
"aria-checked": @checked.to_s }
|
|
37
|
+
input_attrs[:checked] = true if @checked
|
|
38
|
+
input_attrs.merge!(@html_attrs)
|
|
39
|
+
concat content_tag(:input, nil, **input_attrs)
|
|
40
|
+
concat content_tag(:label, nil, for: @id, class: TRACK)
|
|
41
|
+
concat content_tag(:span, nil, class: THUMB, "aria-hidden": "true")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def text_label
|
|
46
|
+
content_tag(:label, @label,
|
|
47
|
+
for: @id,
|
|
48
|
+
class: "cursor-pointer text-sm font-medium leading-none")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<%= tag.div(
|
|
2
|
+
class: cn("w-full", @extra_class),
|
|
3
|
+
data: { controller: "tabs", tabs_index_value: @default_index },
|
|
4
|
+
**@html_attrs
|
|
5
|
+
) do %>
|
|
6
|
+
<div role="tablist"
|
|
7
|
+
class="inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground">
|
|
8
|
+
<% @items_data.each_with_index do |item, i| %>
|
|
9
|
+
<button type="button"
|
|
10
|
+
role="tab"
|
|
11
|
+
data-action="click->tabs#select"
|
|
12
|
+
data-tabs-index-param="<%= i %>"
|
|
13
|
+
data-tabs-target="trigger"
|
|
14
|
+
data-state="<%= i == @default_index ? 'active' : 'inactive' %>"
|
|
15
|
+
aria-selected="<%= i == @default_index %>"
|
|
16
|
+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow">
|
|
17
|
+
<%= item[:title] %>
|
|
18
|
+
</button>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% tabs.each_with_index do |tab, i| %>
|
|
21
|
+
<button type="button"
|
|
22
|
+
role="tab"
|
|
23
|
+
data-action="click->tabs#select"
|
|
24
|
+
data-tabs-index-param="<%= @items_data.size + i %>"
|
|
25
|
+
data-tabs-target="trigger"
|
|
26
|
+
data-state="<%= (@items_data.size + i) == @default_index ? 'active' : 'inactive' %>"
|
|
27
|
+
aria-selected="<%= (@items_data.size + i) == @default_index %>"
|
|
28
|
+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow">
|
|
29
|
+
<%= tab.title %>
|
|
30
|
+
</button>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<% @items_data.each_with_index do |item, i| %>
|
|
35
|
+
<div role="tabpanel"
|
|
36
|
+
data-tabs-target="panel"
|
|
37
|
+
class="mt-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
38
|
+
<%= i != @default_index ? "hidden" : "" %>>
|
|
39
|
+
<%= item[:content] %>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% tabs.each_with_index do |tab, i| %>
|
|
43
|
+
<div role="tabpanel"
|
|
44
|
+
data-tabs-target="panel"
|
|
45
|
+
class="mt-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
46
|
+
<%= (@items_data.size + i) != @default_index ? "hidden" : "" %>>
|
|
47
|
+
<%= tab.call %>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class TabsComponent < ApplicationComponent
|
|
5
|
+
renders_many :tabs, "UI::TabsItemComponent"
|
|
6
|
+
|
|
7
|
+
# items: array shorthand — [{ title:, content: }]
|
|
8
|
+
# default_index: which tab is open on load (0-based)
|
|
9
|
+
def initialize(items: nil, default_index: 0, **html_attrs)
|
|
10
|
+
@items_data = Array(items)
|
|
11
|
+
@default_index = default_index.to_i
|
|
12
|
+
@extra_class = html_attrs.delete(:class)
|
|
13
|
+
@html_attrs = html_attrs
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = { index: Number }
|
|
5
|
+
static targets = ["trigger", "panel"]
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this.#render(this.indexValue)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
select({ params: { index } }) {
|
|
12
|
+
this.indexValue = index
|
|
13
|
+
this.#render(index)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#render(active) {
|
|
17
|
+
this.triggerTargets.forEach((trigger, i) => {
|
|
18
|
+
const isActive = i === active
|
|
19
|
+
trigger.dataset.state = isActive ? "active" : "inactive"
|
|
20
|
+
trigger.setAttribute("aria-selected", isActive)
|
|
21
|
+
})
|
|
22
|
+
this.panelTargets.forEach((panel, i) => {
|
|
23
|
+
panel.hidden = i !== active
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class TextareaComponent < ApplicationComponent
|
|
5
|
+
BASE = "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 " \
|
|
6
|
+
"text-base shadow-xs transition-[color,box-shadow] outline-none " \
|
|
7
|
+
"placeholder:text-muted-foreground " \
|
|
8
|
+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " \
|
|
9
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " \
|
|
10
|
+
"disabled:cursor-not-allowed disabled:opacity-50 " \
|
|
11
|
+
"md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40"
|
|
12
|
+
|
|
13
|
+
def initialize(**html_attrs)
|
|
14
|
+
@extra_class = html_attrs.delete(:class)
|
|
15
|
+
@html_attrs = html_attrs
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
content_tag(:textarea, content,
|
|
20
|
+
class: cn(BASE, @extra_class),
|
|
21
|
+
**@html_attrs)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|