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
|
@@ -4,18 +4,31 @@ module UI
|
|
|
4
4
|
class SwitchComponent < ApplicationComponent
|
|
5
5
|
# sr-only input, track label, and thumb are all siblings inside a relative wrapper
|
|
6
6
|
# so every element can use peer-checked: / peer-focus-visible: / peer-disabled: directly.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
WRAPPERS = {
|
|
8
|
+
default: "relative inline-flex h-[1.15rem] w-8 shrink-0",
|
|
9
|
+
sm: "relative inline-flex h-3.5 w-6 shrink-0"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
TRACK = "absolute inset-0 cursor-pointer rounded-full border border-transparent shadow-xs " \
|
|
13
|
+
"transition-all bg-input peer-checked:bg-primary dark:bg-input/80 " \
|
|
14
|
+
"#{UI::Styles::PEER_FOCUS_RING} " \
|
|
15
|
+
"peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
|
|
16
|
+
|
|
17
|
+
THUMBS = {
|
|
18
|
+
default: "pointer-events-none absolute inset-y-0 left-[1px] my-auto z-10 block size-4 rounded-full " \
|
|
19
|
+
"bg-background ring-0 transition-transform translate-x-0 " \
|
|
20
|
+
"peer-checked:translate-x-[calc(100%-2px)] " \
|
|
21
|
+
"dark:bg-foreground dark:peer-checked:bg-primary-foreground",
|
|
22
|
+
sm: "pointer-events-none absolute inset-y-0 left-[1px] my-auto z-10 block size-3 rounded-full " \
|
|
23
|
+
"bg-background ring-0 transition-transform translate-x-0 " \
|
|
24
|
+
"peer-checked:translate-x-[calc(100%-2px)] " \
|
|
25
|
+
"dark:bg-foreground dark:peer-checked:bg-primary-foreground"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def initialize(label: nil, checked: false, size: :default, **html_attrs)
|
|
17
29
|
@label = label
|
|
18
30
|
@checked = checked
|
|
31
|
+
@size = size.to_sym
|
|
19
32
|
@id = html_attrs[:id] || html_attrs[:name]&.gsub(/[\[\]]+/, "_") || "switch_#{object_id}"
|
|
20
33
|
@extra_class = html_attrs.delete(:class)
|
|
21
34
|
@html_attrs = html_attrs
|
|
@@ -31,21 +44,21 @@ module UI
|
|
|
31
44
|
private
|
|
32
45
|
|
|
33
46
|
def switch_widget
|
|
34
|
-
content_tag(:div, class:
|
|
47
|
+
content_tag(:div, class: WRAPPERS.fetch(@size, WRAPPERS[:default])) do
|
|
35
48
|
input_attrs = { type: "checkbox", id: @id, class: "peer sr-only", role: "switch",
|
|
36
|
-
"aria-checked": @checked.to_s }
|
|
49
|
+
"aria-checked": @checked.to_s, "data-size": @size.to_s }
|
|
37
50
|
input_attrs[:checked] = true if @checked
|
|
38
51
|
input_attrs.merge!(@html_attrs)
|
|
39
52
|
concat content_tag(:input, nil, **input_attrs)
|
|
40
53
|
concat content_tag(:label, nil, for: @id, class: TRACK)
|
|
41
|
-
concat content_tag(:span, nil, class:
|
|
54
|
+
concat content_tag(:span, nil, class: THUMBS.fetch(@size, THUMBS[:default]), "aria-hidden": "true")
|
|
42
55
|
end
|
|
43
56
|
end
|
|
44
57
|
|
|
45
58
|
def text_label
|
|
46
59
|
content_tag(:label, @label,
|
|
47
60
|
for: @id,
|
|
48
|
-
class: "cursor-pointer text-sm font-medium leading-none")
|
|
61
|
+
class: "cursor-pointer text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50")
|
|
49
62
|
end
|
|
50
63
|
end
|
|
51
64
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<%= tag.div(
|
|
2
|
-
class: cn("w-full", @extra_class),
|
|
3
|
-
data: { controller: "tabs", tabs_index_value: @default_index },
|
|
2
|
+
class: cn("group/tabs flex w-full flex-col gap-2", @extra_class),
|
|
3
|
+
data: { slot: "tabs", controller: "tabs", tabs_index_value: @default_index },
|
|
4
4
|
**@html_attrs
|
|
5
5
|
) do %>
|
|
6
6
|
<div role="tablist"
|
|
7
|
-
|
|
7
|
+
data-slot="tabs-list"
|
|
8
|
+
data-variant="default"
|
|
9
|
+
class="group/tabs-list inline-flex h-9 w-fit items-center justify-center rounded-lg bg-muted p-[3px] text-muted-foreground">
|
|
8
10
|
<% @items_data.each_with_index do |item, i| %>
|
|
9
11
|
<button type="button"
|
|
10
12
|
role="tab"
|
|
@@ -13,7 +15,8 @@
|
|
|
13
15
|
data-tabs-target="trigger"
|
|
14
16
|
data-state="<%= i == @default_index ? 'active' : 'inactive' %>"
|
|
15
17
|
aria-selected="<%= i == @default_index %>"
|
|
16
|
-
|
|
18
|
+
data-slot="tabs-trigger"
|
|
19
|
+
class="relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all hover:text-foreground vp-focus-ring focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm data-[state=active]:bg-background data-[state=active]:text-foreground dark:text-muted-foreground dark:hover:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
|
17
20
|
<%= item[:title] %>
|
|
18
21
|
</button>
|
|
19
22
|
<% end %>
|
|
@@ -25,7 +28,8 @@
|
|
|
25
28
|
data-tabs-target="trigger"
|
|
26
29
|
data-state="<%= (@items_data.size + i) == @default_index ? 'active' : 'inactive' %>"
|
|
27
30
|
aria-selected="<%= (@items_data.size + i) == @default_index %>"
|
|
28
|
-
|
|
31
|
+
data-slot="tabs-trigger"
|
|
32
|
+
class="relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all hover:text-foreground vp-focus-ring focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm data-[state=active]:bg-background data-[state=active]:text-foreground dark:text-muted-foreground dark:hover:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
|
29
33
|
<%= tab.title %>
|
|
30
34
|
</button>
|
|
31
35
|
<% end %>
|
|
@@ -33,16 +37,18 @@
|
|
|
33
37
|
|
|
34
38
|
<% @items_data.each_with_index do |item, i| %>
|
|
35
39
|
<div role="tabpanel"
|
|
40
|
+
data-slot="tabs-content"
|
|
36
41
|
data-tabs-target="panel"
|
|
37
|
-
class="
|
|
42
|
+
class="flex-1 outline-none"
|
|
38
43
|
<%= i != @default_index ? "hidden" : "" %>>
|
|
39
44
|
<%= item[:content] %>
|
|
40
45
|
</div>
|
|
41
46
|
<% end %>
|
|
42
47
|
<% tabs.each_with_index do |tab, i| %>
|
|
43
48
|
<div role="tabpanel"
|
|
49
|
+
data-slot="tabs-content"
|
|
44
50
|
data-tabs-target="panel"
|
|
45
|
-
class="
|
|
51
|
+
class="flex-1 outline-none"
|
|
46
52
|
<%= (@items_data.size + i) != @default_index ? "hidden" : "" %>>
|
|
47
53
|
<%= tab.call %>
|
|
48
54
|
</div>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module UI
|
|
4
|
+
class TagsInputComponent < ApplicationComponent
|
|
5
|
+
TRIGGER = "flex flex-wrap items-center gap-1.5 min-h-9 w-full rounded-md border border-input " \
|
|
6
|
+
"bg-transparent px-3 py-1.5 text-sm shadow-xs cursor-text " \
|
|
7
|
+
"focus-within:outline-none focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50"
|
|
8
|
+
CHIP = "inline-flex items-center gap-1 rounded-md bg-secondary " \
|
|
9
|
+
"text-secondary-foreground px-2 py-0.5 text-xs font-medium"
|
|
10
|
+
CHIP_LABEL = "tags-input-chip-label"
|
|
11
|
+
REMOVE = "ml-0.5 rounded-xs opacity-60 transition-opacity hover:opacity-100 #{UI::Styles::FOCUS_RING}"
|
|
12
|
+
FILTER = "min-w-[80px] flex-1 bg-transparent text-sm outline-none " \
|
|
13
|
+
"placeholder:text-muted-foreground"
|
|
14
|
+
PANEL = "#{UI::Styles::POPOVER_PANEL} top-full left-0 mt-1 w-full overflow-hidden"
|
|
15
|
+
LIST = "max-h-[200px] overflow-y-auto p-1"
|
|
16
|
+
OPTION = "#{UI::Styles::MENU_ITEM} w-full cursor-pointer " \
|
|
17
|
+
"hover:bg-accent hover:text-accent-foreground"
|
|
18
|
+
EMPTY = "py-4 text-center text-sm text-muted-foreground"
|
|
19
|
+
|
|
20
|
+
def initialize(name:, options: [], values: [], placeholder: "Select...", **html_attrs)
|
|
21
|
+
@name = name
|
|
22
|
+
@options = options
|
|
23
|
+
@values = Array(values).map(&:to_s)
|
|
24
|
+
@placeholder = placeholder
|
|
25
|
+
@extra_class = html_attrs.delete(:class)
|
|
26
|
+
@html_attrs = html_attrs
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call
|
|
30
|
+
content_tag(:div,
|
|
31
|
+
class: cn("relative", @extra_class),
|
|
32
|
+
data: {
|
|
33
|
+
controller: "tags-input",
|
|
34
|
+
action: "click@document->tags-input#closeOnClickOutside"
|
|
35
|
+
},
|
|
36
|
+
**@html_attrs) do
|
|
37
|
+
concat trigger
|
|
38
|
+
concat chip_template
|
|
39
|
+
concat dropdown
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def trigger
|
|
46
|
+
content_tag(:div,
|
|
47
|
+
class: TRIGGER,
|
|
48
|
+
data: {"tags-input-target": "trigger", action: "click->tags-input#focusInput"}) do
|
|
49
|
+
concat safe_join(selected_options.map { |opt| chip(opt) })
|
|
50
|
+
concat filter_input
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def chip(opt)
|
|
55
|
+
content_tag(:span,
|
|
56
|
+
class: CHIP,
|
|
57
|
+
data: {"tags-input-target": "chip", "tags-input-value": opt[:value]}) do
|
|
58
|
+
concat content_tag(:span, opt[:label], class: CHIP_LABEL)
|
|
59
|
+
concat tag.input(type: "hidden", name: "#{@name}[]", value: opt[:value])
|
|
60
|
+
concat remove_button(opt[:value])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def chip_template
|
|
65
|
+
content_tag(:template, data: {"tags-input-target": "chipTemplate"}) do
|
|
66
|
+
chip(value: "", label: "")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def remove_button(value)
|
|
71
|
+
content_tag(:button,
|
|
72
|
+
"×",
|
|
73
|
+
type: "button",
|
|
74
|
+
"aria-label": "Remove",
|
|
75
|
+
class: REMOVE,
|
|
76
|
+
data: {"tags-input-value": value, action: "click->tags-input#remove"})
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def filter_input
|
|
80
|
+
tag.input(
|
|
81
|
+
type: "text",
|
|
82
|
+
placeholder: selected_options.empty? ? @placeholder : nil,
|
|
83
|
+
autocomplete: "off",
|
|
84
|
+
class: FILTER,
|
|
85
|
+
data: {
|
|
86
|
+
"tags-input-target": "input",
|
|
87
|
+
action: "focus->tags-input#open input->tags-input#filter keydown->tags-input#keydown"
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def dropdown
|
|
93
|
+
content_tag(:div,
|
|
94
|
+
data: {"tags-input-target": "panel"},
|
|
95
|
+
hidden: true,
|
|
96
|
+
class: PANEL) do
|
|
97
|
+
concat content_tag(:div, class: LIST) {
|
|
98
|
+
concat options_list
|
|
99
|
+
concat content_tag(:div, "No results.",
|
|
100
|
+
class: EMPTY,
|
|
101
|
+
data: {"tags-input-target": "empty"},
|
|
102
|
+
hidden: true)
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def options_list
|
|
108
|
+
safe_join(normalized_options.map { |opt|
|
|
109
|
+
content_tag(:button, opt[:label],
|
|
110
|
+
type: "button",
|
|
111
|
+
class: OPTION,
|
|
112
|
+
hidden: @values.include?(opt[:value]),
|
|
113
|
+
data: {
|
|
114
|
+
"tags-input-target": "option",
|
|
115
|
+
"tags-input-value": opt[:value],
|
|
116
|
+
"tags-input-label": opt[:label],
|
|
117
|
+
action: "click->tags-input#select"
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def normalized_options
|
|
123
|
+
@options.map { |o|
|
|
124
|
+
case o
|
|
125
|
+
when Hash then {value: o[:value].to_s, label: o[:label].to_s}
|
|
126
|
+
when Array then {value: o[0].to_s, label: o[1].to_s}
|
|
127
|
+
else {value: o.to_s, label: o.to_s}
|
|
128
|
+
end
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def selected_options
|
|
133
|
+
normalized_options.select { |o| @values.include?(o[:value]) }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["input", "panel", "option", "empty", "chip", "chipTemplate"]
|
|
5
|
+
|
|
6
|
+
open() {
|
|
7
|
+
this.panelTarget.hidden = false
|
|
8
|
+
this.filter()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
close() {
|
|
12
|
+
this.panelTarget.hidden = true
|
|
13
|
+
this.inputTarget.value = ""
|
|
14
|
+
this.#resetOptionVisibility()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
filter() {
|
|
18
|
+
const query = this.inputTarget.value.toLowerCase()
|
|
19
|
+
let visible = 0
|
|
20
|
+
this.optionTargets.forEach(option => {
|
|
21
|
+
if (option.hidden) return
|
|
22
|
+
const match = option.dataset.tagsInputLabel.toLowerCase().includes(query)
|
|
23
|
+
option.style.display = match ? "" : "none"
|
|
24
|
+
if (match) visible++
|
|
25
|
+
})
|
|
26
|
+
if (this.hasEmptyTarget) this.emptyTarget.hidden = visible > 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
select(event) {
|
|
30
|
+
const { tagsInputValue, tagsInputLabel } = event.currentTarget.dataset
|
|
31
|
+
event.currentTarget.hidden = true
|
|
32
|
+
event.currentTarget.style.display = ""
|
|
33
|
+
this.#addChip(tagsInputValue, tagsInputLabel)
|
|
34
|
+
this.inputTarget.value = ""
|
|
35
|
+
this.filter()
|
|
36
|
+
this.inputTarget.focus()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
remove(event) {
|
|
40
|
+
event.stopPropagation()
|
|
41
|
+
const value = event.currentTarget.dataset.tagsInputValue
|
|
42
|
+
const option = this.optionTargets.find(o => o.dataset.tagsInputValue === value)
|
|
43
|
+
if (option) { option.hidden = false; option.style.display = "" }
|
|
44
|
+
event.currentTarget.closest("[data-tags-input-target~='chip']").remove()
|
|
45
|
+
this.filter()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
focusInput() {
|
|
49
|
+
this.inputTarget.focus()
|
|
50
|
+
if (this.panelTarget.hidden) this.open()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
keydown(event) {
|
|
54
|
+
if (event.key === "Backspace" && this.inputTarget.value === "") {
|
|
55
|
+
const chips = this.chipTargets
|
|
56
|
+
if (chips.length > 0) {
|
|
57
|
+
chips[chips.length - 1].querySelector("[data-action*='tags-input#remove']")?.click()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (event.key === "Escape") this.close()
|
|
61
|
+
if (event.key === "Enter") {
|
|
62
|
+
event.preventDefault()
|
|
63
|
+
const first = this.optionTargets.find(o => !o.hidden && o.style.display !== "none")
|
|
64
|
+
if (first) first.click()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
closeOnClickOutside({ target }) {
|
|
69
|
+
if (!this.element.contains(target)) this.close()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#addChip(value, label) {
|
|
73
|
+
const chip = this.chipTemplateTarget.content.cloneNode(true).firstElementChild
|
|
74
|
+
chip.dataset.tagsInputValue = value
|
|
75
|
+
chip.querySelector(".tags-input-chip-label").textContent = label
|
|
76
|
+
const hidden = chip.querySelector("input[type='hidden']")
|
|
77
|
+
if (hidden) hidden.value = value
|
|
78
|
+
const btn = chip.querySelector("[data-action*='tags-input#remove']")
|
|
79
|
+
if (btn) btn.dataset.tagsInputValue = value
|
|
80
|
+
this.inputTarget.insertAdjacentElement("beforebegin", chip)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#resetOptionVisibility() {
|
|
84
|
+
const selected = new Set(this.chipTargets.map(c => c.dataset.tagsInputValue))
|
|
85
|
+
this.optionTargets.forEach(o => {
|
|
86
|
+
o.hidden = selected.has(o.dataset.tagsInputValue)
|
|
87
|
+
o.style.display = ""
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -2,22 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
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
5
|
def initialize(**html_attrs)
|
|
14
|
-
|
|
15
|
-
@html_attrs = html_attrs
|
|
6
|
+
extract_html_attrs(**html_attrs)
|
|
16
7
|
end
|
|
17
8
|
|
|
18
9
|
def call
|
|
19
10
|
content_tag(:textarea, content,
|
|
20
|
-
class: cn(
|
|
11
|
+
class: cn(UI::Styles::TEXTAREA, @extra_class),
|
|
21
12
|
**@html_attrs)
|
|
22
13
|
end
|
|
23
14
|
end
|
|
@@ -21,7 +21,8 @@ module UI
|
|
|
21
21
|
|
|
22
22
|
def call
|
|
23
23
|
content_tag(:ol,
|
|
24
|
-
class: cn("relative border-l border-border
|
|
24
|
+
class: cn("relative ml-3 border-l border-border/60", @extra_class),
|
|
25
|
+
data: { slot: "timeline" },
|
|
25
26
|
**@html_attrs) do
|
|
26
27
|
safe_join(items)
|
|
27
28
|
end
|
|
@@ -31,15 +32,15 @@ module UI
|
|
|
31
32
|
# variant: :default | :success | :warning | :destructive | :muted
|
|
32
33
|
VARIANTS = {
|
|
33
34
|
default: "bg-primary",
|
|
34
|
-
success: "bg-
|
|
35
|
-
warning: "bg-
|
|
35
|
+
success: "bg-chart-2",
|
|
36
|
+
warning: "bg-chart-4",
|
|
36
37
|
destructive: "bg-destructive",
|
|
37
38
|
muted: "bg-muted-foreground"
|
|
38
39
|
}.freeze
|
|
39
40
|
|
|
40
|
-
DOT_CLS = "absolute -left-
|
|
41
|
-
DATE_CLS = "mb-
|
|
42
|
-
TITLE_CLS = "text-sm font-medium text-foreground
|
|
41
|
+
DOT_CLS = "absolute -left-[7px] mt-1.5 size-3 shrink-0 rounded-full border-2 border-background"
|
|
42
|
+
DATE_CLS = "mb-1 text-xs text-muted-foreground"
|
|
43
|
+
TITLE_CLS = "text-sm font-medium leading-snug text-foreground"
|
|
43
44
|
DESC_CLS = "mt-1 text-sm text-muted-foreground"
|
|
44
45
|
|
|
45
46
|
# date: optional date/time string shown above the title
|
|
@@ -57,7 +58,8 @@ module UI
|
|
|
57
58
|
|
|
58
59
|
def call
|
|
59
60
|
content_tag(:li,
|
|
60
|
-
class: cn("mb-
|
|
61
|
+
class: cn("mb-10 ml-6 last:mb-0", @extra_class),
|
|
62
|
+
data: { slot: "timeline-item" },
|
|
61
63
|
**@html_attrs) do
|
|
62
64
|
concat dot
|
|
63
65
|
concat content_tag(:time, @date, class: DATE_CLS) if @date
|
|
@@ -10,21 +10,23 @@ module UI
|
|
|
10
10
|
# step: minute step increment (default 1, common: 5, 15, 30)
|
|
11
11
|
|
|
12
12
|
WRAPPER = "relative inline-block"
|
|
13
|
-
TRIGGER = "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
ICON_CLS = "size-4 shrink-0 text-muted-foreground"
|
|
18
|
-
POPOVER = "absolute left-0 top-full z-50 mt-1 hidden w-max rounded-lg border border-border " \
|
|
19
|
-
"bg-popover p-3 shadow-md data-[open=true]:block"
|
|
13
|
+
TRIGGER = "#{UI::Styles::PICKER_TRIGGER} w-36"
|
|
14
|
+
ICON_CLS = "size-4 shrink-0 text-muted-foreground pointer-events-none"
|
|
15
|
+
LABEL_PLACEHOLDER = "text-muted-foreground"
|
|
16
|
+
POPOVER = "absolute left-0 top-full z-50 mt-2 hidden w-max p-3 #{UI::Styles::FIELD_PANEL} data-[open=true]:block"
|
|
20
17
|
SPINNER_WRAP = "flex items-center justify-center gap-1"
|
|
21
18
|
COL_CLS = "flex flex-col items-center gap-1"
|
|
22
|
-
SPIN_BTN = "inline-flex size-7 items-center justify-center rounded-md " \
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
19
|
+
SPIN_BTN = "inline-flex size-7 items-center justify-center rounded-md text-muted-foreground " \
|
|
20
|
+
"transition-colors outline-none " \
|
|
21
|
+
"hover:bg-accent hover:text-accent-foreground " \
|
|
22
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
23
|
+
"dark:hover:bg-accent/50"
|
|
24
|
+
NUM_CLS = "h-9 w-10 rounded-md border border-input bg-transparent px-1 py-1 text-center text-sm shadow-xs " \
|
|
25
|
+
"transition-[color,box-shadow] outline-none " \
|
|
26
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
27
|
+
"dark:bg-input/30"
|
|
28
|
+
AMPM_CLS = NUM_CLS + " cursor-pointer select-none"
|
|
29
|
+
SEP_CLS = "pb-1 text-lg font-medium text-foreground"
|
|
28
30
|
|
|
29
31
|
def initialize(value: nil, name: nil, format: :h24, step: 1, **html_attrs)
|
|
30
32
|
@value = value
|
|
@@ -68,7 +70,9 @@ module UI
|
|
|
68
70
|
action: "click->timepicker#toggle"
|
|
69
71
|
}) do
|
|
70
72
|
concat clock_icon
|
|
71
|
-
concat content_tag(:span, @value || "Pick time",
|
|
73
|
+
concat content_tag(:span, @value || "Pick time",
|
|
74
|
+
class: (@value ? nil : LABEL_PLACEHOLDER),
|
|
75
|
+
data: { timepicker_target: "label" })
|
|
72
76
|
end
|
|
73
77
|
end
|
|
74
78
|
|
|
@@ -113,7 +117,7 @@ module UI
|
|
|
113
117
|
content_tag(:div, class: COL_CLS) do
|
|
114
118
|
concat spin_btn("▲", "click->timepicker#toggleAmPm")
|
|
115
119
|
concat content_tag(:span, "AM",
|
|
116
|
-
class:
|
|
120
|
+
class: AMPM_CLS,
|
|
117
121
|
data: { timepicker_target: "ampm", action: "click->timepicker#toggleAmPm" })
|
|
118
122
|
concat spin_btn("▼", "click->timepicker#toggleAmPm")
|
|
119
123
|
end
|
|
@@ -25,7 +25,7 @@ module UI
|
|
|
25
25
|
top_center: "fixed top-4 left-1/2 -translate-x-1/2"
|
|
26
26
|
}.freeze
|
|
27
27
|
|
|
28
|
-
CONTAINER_CLS = "z-50 flex flex-col gap-
|
|
28
|
+
CONTAINER_CLS = "toaster group z-50 flex w-80 flex-col gap-3 pointer-events-none"
|
|
29
29
|
|
|
30
30
|
renders_many :toasts, "UI::ToasterComponent::ToastComponent"
|
|
31
31
|
|
|
@@ -53,14 +53,14 @@ module UI
|
|
|
53
53
|
VARIANTS = {
|
|
54
54
|
default: {border: "border-border", icon: nil, icon_color: "text-foreground"},
|
|
55
55
|
success: {
|
|
56
|
-
border: "border-
|
|
56
|
+
border: "border-chart-2/40",
|
|
57
57
|
icon: "M9 12l2 2 4-4m6 2a9 9 0 1 1-18 0 9 9 0 0 1 18 0z",
|
|
58
|
-
icon_color: "text-
|
|
58
|
+
icon_color: "text-chart-2"
|
|
59
59
|
},
|
|
60
60
|
warning: {
|
|
61
|
-
border: "border-
|
|
61
|
+
border: "border-chart-4/40",
|
|
62
62
|
icon: "M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z",
|
|
63
|
-
icon_color: "text-
|
|
63
|
+
icon_color: "text-chart-4"
|
|
64
64
|
},
|
|
65
65
|
destructive: {
|
|
66
66
|
border: "border-destructive/40",
|
|
@@ -68,20 +68,20 @@ module UI
|
|
|
68
68
|
icon_color: "text-destructive"
|
|
69
69
|
},
|
|
70
70
|
info: {
|
|
71
|
-
border: "border-
|
|
71
|
+
border: "border-border",
|
|
72
72
|
icon: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z",
|
|
73
|
-
icon_color: "text-
|
|
73
|
+
icon_color: "text-muted-foreground"
|
|
74
74
|
}
|
|
75
75
|
}.freeze
|
|
76
76
|
|
|
77
|
-
TOAST_CLS = "pointer-events-auto flex items-start gap-3 rounded-
|
|
78
|
-
"bg-
|
|
77
|
+
TOAST_CLS = "pointer-events-auto flex items-start gap-3 rounded-md #{UI::Styles::BORDER} " \
|
|
78
|
+
"bg-popover px-4 py-3 text-popover-foreground shadow-md outline-hidden " \
|
|
79
79
|
"transition-all duration-300 translate-y-2 opacity-0 " \
|
|
80
80
|
"data-[open=true]:translate-y-0 data-[open=true]:opacity-100"
|
|
81
81
|
|
|
82
82
|
CLOSE_CLS = "ml-auto -mr-1 -mt-0.5 shrink-0 inline-flex size-6 items-center justify-center " \
|
|
83
83
|
"rounded-md text-muted-foreground hover:text-foreground hover:bg-accent " \
|
|
84
|
-
"
|
|
84
|
+
"#{UI::Styles::FOCUS_RING} outline-hidden transition"
|
|
85
85
|
|
|
86
86
|
# message: toast body (required)
|
|
87
87
|
# title: optional bold heading
|
|
@@ -40,10 +40,10 @@ export default class extends Controller {
|
|
|
40
40
|
#buildToast({ message = "", title = "", variant = "default", duration = 4000 }) {
|
|
41
41
|
const borderCls = {
|
|
42
42
|
default: "border-border",
|
|
43
|
-
success: "border-
|
|
44
|
-
warning: "border-
|
|
43
|
+
success: "border-chart-2/40",
|
|
44
|
+
warning: "border-chart-4/40",
|
|
45
45
|
destructive: "border-destructive/40",
|
|
46
|
-
info: "border-
|
|
46
|
+
info: "border-border"
|
|
47
47
|
}[variant] ?? "border-border"
|
|
48
48
|
|
|
49
49
|
const div = document.createElement("div")
|
|
@@ -53,8 +53,8 @@ export default class extends Controller {
|
|
|
53
53
|
div.dataset.toasterTarget = "toast"
|
|
54
54
|
div.dataset.toasterDurationParam = duration
|
|
55
55
|
div.className = [
|
|
56
|
-
"pointer-events-auto flex items-start gap-3 rounded-
|
|
57
|
-
"bg-
|
|
56
|
+
"pointer-events-auto flex items-start gap-3 rounded-md vp-border",
|
|
57
|
+
"bg-popover px-4 py-3 text-popover-foreground shadow-md outline-hidden",
|
|
58
58
|
"transition-all duration-300 translate-y-2 opacity-0",
|
|
59
59
|
"data-[open=true]:translate-y-0 data-[open=true]:opacity-100",
|
|
60
60
|
borderCls
|
|
@@ -71,7 +71,7 @@ export default class extends Controller {
|
|
|
71
71
|
data-action="click->toaster#dismiss"
|
|
72
72
|
class="ml-auto -mr-1 -mt-0.5 shrink-0 inline-flex size-6 items-center justify-center
|
|
73
73
|
rounded-md text-muted-foreground hover:text-foreground hover:bg-accent
|
|
74
|
-
|
|
74
|
+
vp-focus-ring outline-hidden transition">
|
|
75
75
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
|
|
76
76
|
stroke="currentColor" stroke-width="2" class="size-3.5" aria-hidden="true">
|
|
77
77
|
<path d="M18 6 6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
@@ -4,21 +4,27 @@ module UI
|
|
|
4
4
|
class ToggleComponent < ApplicationComponent
|
|
5
5
|
BASE = "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap " \
|
|
6
6
|
"transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground " \
|
|
7
|
-
"
|
|
7
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
8
8
|
"disabled:pointer-events-none disabled:opacity-50 " \
|
|
9
9
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " \
|
|
10
10
|
"data-[state=on]:bg-accent data-[state=on]:text-accent-foreground " \
|
|
11
11
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
12
12
|
|
|
13
|
+
VARIANTS = {
|
|
14
|
+
default: "bg-transparent",
|
|
15
|
+
outline: "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
13
18
|
SIZES = {
|
|
14
19
|
default: "h-9 min-w-9 px-2",
|
|
15
20
|
sm: "h-8 min-w-8 px-1.5",
|
|
16
21
|
lg: "h-10 min-w-10 px-2.5"
|
|
17
22
|
}.freeze
|
|
18
23
|
|
|
19
|
-
def initialize(label = nil, pressed: false, size: :default, value: nil, **html_attrs)
|
|
24
|
+
def initialize(label = nil, pressed: false, variant: :default, size: :default, value: nil, **html_attrs)
|
|
20
25
|
@label = label || html_attrs.delete(:label)
|
|
21
26
|
@pressed = pressed
|
|
27
|
+
@variant = variant.to_sym
|
|
22
28
|
@size = size.to_sym
|
|
23
29
|
@value = value
|
|
24
30
|
@extra_class = html_attrs.delete(:class)
|
|
@@ -34,7 +40,8 @@ module UI
|
|
|
34
40
|
"data-controller": "toggle",
|
|
35
41
|
"data-action": "click->toggle#toggle",
|
|
36
42
|
value: @value,
|
|
37
|
-
class: cn(BASE,
|
|
43
|
+
class: cn(BASE, VARIANTS.fetch(@variant, VARIANTS[:default]),
|
|
44
|
+
SIZES.fetch(@size, SIZES[:default]), @extra_class),
|
|
38
45
|
**@html_attrs)
|
|
39
46
|
end
|
|
40
47
|
end
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class ToggleGroupComponent < ApplicationComponent
|
|
5
|
-
BASE = "
|
|
5
|
+
BASE = "group/toggle-group flex w-fit items-center gap-1 rounded-md " \
|
|
6
|
+
"data-[variant=outline]:shadow-xs"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
# :multiple — multiple items can be active simultaneously
|
|
9
|
-
# value: currently active value (String) for :single,
|
|
10
|
-
# or array of active values for :multiple
|
|
11
|
-
def initialize(type: :single, value: nil, **html_attrs)
|
|
8
|
+
def initialize(type: :single, value: nil, variant: :default, **html_attrs)
|
|
12
9
|
@type = type.to_sym
|
|
13
10
|
@value = Array(value).map(&:to_s)
|
|
11
|
+
@variant = variant.to_sym
|
|
14
12
|
@extra_class = html_attrs.delete(:class)
|
|
15
13
|
@html_attrs = html_attrs
|
|
16
14
|
end
|
|
@@ -20,8 +18,10 @@ module UI
|
|
|
20
18
|
content,
|
|
21
19
|
class: cn(BASE, @extra_class),
|
|
22
20
|
role: "group",
|
|
21
|
+
"data-variant": @variant.to_s,
|
|
23
22
|
"data-controller": "toggle-group",
|
|
24
23
|
"data-toggle-group-type-value": @type,
|
|
24
|
+
"data-slot": "toggle-group",
|
|
25
25
|
**@html_attrs)
|
|
26
26
|
end
|
|
27
27
|
|