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
|
@@ -8,19 +8,17 @@ module UI
|
|
|
8
8
|
# Usage:
|
|
9
9
|
# <%%= ui :input_otp, length: 6, name: "otp" %>
|
|
10
10
|
|
|
11
|
-
CELL_CLS = "h-
|
|
12
|
-
"shadow-xs transition-
|
|
11
|
+
CELL_CLS = "relative flex h-9 w-9 items-center justify-center rounded-md border border-input " \
|
|
12
|
+
"bg-transparent text-center text-sm shadow-xs transition-all outline-none " \
|
|
13
13
|
"caret-transparent selection:bg-primary selection:text-primary-foreground " \
|
|
14
|
-
"
|
|
14
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
15
15
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 " \
|
|
16
|
-
"
|
|
16
|
+
"dark:bg-input/30 dark:aria-invalid:ring-destructive/40 " \
|
|
17
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
17
18
|
|
|
18
|
-
WRAPPER_CLS = "flex items-center gap-2"
|
|
19
|
-
SEPARATOR_CLS = "text-muted-foreground
|
|
19
|
+
WRAPPER_CLS = "flex items-center gap-2 has-[:disabled]:opacity-50"
|
|
20
|
+
SEPARATOR_CLS = "text-muted-foreground"
|
|
20
21
|
|
|
21
|
-
# length: number of OTP digits (default: 6)
|
|
22
|
-
# name: form field name (individual cells get name[0], name[1], …)
|
|
23
|
-
# separator: position (Integer) or Hash { position => char }, e.g. 3 or { 3 => "-" }
|
|
24
22
|
def initialize(length: 6, name: "otp", separator: nil, **html_attrs)
|
|
25
23
|
@length = length.to_i
|
|
26
24
|
@name = name
|
|
@@ -35,11 +33,12 @@ module UI
|
|
|
35
33
|
def call
|
|
36
34
|
content_tag(:div,
|
|
37
35
|
class: cn(WRAPPER_CLS, @extra_class),
|
|
36
|
+
"data-slot": "input-otp",
|
|
38
37
|
data: { controller: "input-otp" },
|
|
39
38
|
**@html_attrs) do
|
|
40
39
|
@length.times do |i|
|
|
41
40
|
sep = @separator&.fetch(i, nil)
|
|
42
|
-
concat content_tag(:
|
|
41
|
+
concat content_tag(:div, sep == "-" ? separator_icon : sep, class: SEPARATOR_CLS, role: "separator") if sep
|
|
43
42
|
concat digit_input(i)
|
|
44
43
|
end
|
|
45
44
|
end
|
|
@@ -47,6 +46,18 @@ module UI
|
|
|
47
46
|
|
|
48
47
|
private
|
|
49
48
|
|
|
49
|
+
def separator_icon
|
|
50
|
+
content_tag(:svg,
|
|
51
|
+
content_tag(:path, nil, d: "M5 12h14", "stroke-linecap": "round", "stroke-linejoin": "round"),
|
|
52
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
53
|
+
viewBox: "0 0 24 24",
|
|
54
|
+
fill: "none",
|
|
55
|
+
stroke: "currentColor",
|
|
56
|
+
"stroke-width": "2",
|
|
57
|
+
class: "size-4",
|
|
58
|
+
"aria-hidden": "true")
|
|
59
|
+
end
|
|
60
|
+
|
|
50
61
|
def digit_input(index)
|
|
51
62
|
content_tag(:input, nil,
|
|
52
63
|
type: "text",
|
|
@@ -55,6 +66,7 @@ module UI
|
|
|
55
66
|
autocomplete: index.zero? ? "one-time-code" : "off",
|
|
56
67
|
name: "#{@name}[#{index}]",
|
|
57
68
|
class: CELL_CLS,
|
|
69
|
+
"data-slot": "input-otp-slot",
|
|
58
70
|
"aria-label": "Digit #{index + 1}",
|
|
59
71
|
data: {
|
|
60
72
|
input_otp_target: "cell",
|
|
@@ -4,7 +4,9 @@ module UI
|
|
|
4
4
|
class KbdComponent < ApplicationComponent
|
|
5
5
|
BASE = "pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 " \
|
|
6
6
|
"rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none " \
|
|
7
|
-
"[&_svg:not([class*='size-'])]:size-3"
|
|
7
|
+
"[&_svg:not([class*='size-'])]:size-3 " \
|
|
8
|
+
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background " \
|
|
9
|
+
"dark:[[data-slot=tooltip-content]_&]:bg-background/10"
|
|
8
10
|
|
|
9
11
|
def initialize(key = nil, **html_attrs)
|
|
10
12
|
@key = key || html_attrs.delete(:label)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class ListGroupComponent < ApplicationComponent
|
|
5
|
-
BASE = "divide-y divide-border overflow-hidden rounded-
|
|
5
|
+
BASE = "flex flex-col divide-y divide-border overflow-hidden rounded-md #{UI::Styles::BORDER}"
|
|
6
6
|
|
|
7
7
|
def initialize(**html_attrs)
|
|
8
8
|
@extra_class = html_attrs.delete(:class)
|
|
@@ -10,7 +10,11 @@ module UI
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def call
|
|
13
|
-
content_tag(:ul, content,
|
|
13
|
+
content_tag(:ul, content,
|
|
14
|
+
class: cn(BASE, @extra_class),
|
|
15
|
+
role: "list",
|
|
16
|
+
data: { slot: "item-group" },
|
|
17
|
+
**@html_attrs)
|
|
14
18
|
end
|
|
15
19
|
end
|
|
16
20
|
end
|
data/lib/generators/view_primitives/add/templates/list_group/list_group_item_component.rb.tt
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class ListGroupItemComponent < ApplicationComponent
|
|
5
|
-
BASE = "flex items-center justify-between px-4 py-3 text-sm"
|
|
5
|
+
BASE = "flex items-center justify-between gap-4 px-4 py-3 text-sm transition-colors duration-100 " \
|
|
6
|
+
"outline-none #{UI::Styles::FOCUS_RING}"
|
|
6
7
|
|
|
7
8
|
VARIANTS = {
|
|
8
|
-
default: "text-foreground hover:bg-
|
|
9
|
-
active: "bg-
|
|
10
|
-
muted: "text-muted-foreground hover:bg-
|
|
9
|
+
default: "text-foreground hover:bg-accent/50",
|
|
10
|
+
active: "bg-accent font-medium text-accent-foreground",
|
|
11
|
+
muted: "text-muted-foreground hover:bg-accent/50"
|
|
11
12
|
}.freeze
|
|
12
13
|
|
|
13
14
|
def initialize(label = nil, href: nil, active: false, variant: :default, **html_attrs)
|
|
@@ -24,6 +25,7 @@ module UI
|
|
|
24
25
|
content_tag(tag_name,
|
|
25
26
|
content.presence || @label,
|
|
26
27
|
class: cn(BASE, VARIANTS.fetch(@variant, VARIANTS[:default]), @extra_class),
|
|
28
|
+
data: { slot: "item" },
|
|
27
29
|
**extra,
|
|
28
30
|
**@html_attrs)
|
|
29
31
|
end
|
|
@@ -23,7 +23,8 @@ module UI
|
|
|
23
23
|
# target: link target, e.g. "_blank"
|
|
24
24
|
# rel: link rel attribute
|
|
25
25
|
|
|
26
|
-
WRAPPER_CLS = "relative inline-block"
|
|
26
|
+
WRAPPER_CLS = "relative inline-block overflow-hidden rounded-md #{UI::Styles::BORDER} shadow-xs"
|
|
27
|
+
IMG_CLS = "block h-auto w-full max-w-full rounded-md"
|
|
27
28
|
|
|
28
29
|
def initialize(src:, alt:, areas: [], width: nil, height: nil,
|
|
29
30
|
loading: :lazy, map_name: nil, **html_attrs)
|
|
@@ -47,7 +48,7 @@ module UI
|
|
|
47
48
|
private
|
|
48
49
|
|
|
49
50
|
def img_tag
|
|
50
|
-
attrs = { src: @src, alt: @alt, usemap: "##{@map_name}", loading: @loading }
|
|
51
|
+
attrs = { src: @src, alt: @alt, usemap: "##{@map_name}", loading: @loading, class: IMG_CLS }
|
|
51
52
|
attrs[:width] = @width if @width
|
|
52
53
|
attrs[:height] = @height if @height
|
|
53
54
|
tag.img(**attrs)
|
|
@@ -5,25 +5,25 @@ module UI
|
|
|
5
5
|
# Full-width dropdown panel anchored to a trigger button.
|
|
6
6
|
# Columns are rendered via with_column blocks.
|
|
7
7
|
|
|
8
|
-
TRIGGER_CLS = "inline-flex h-9 items-center justify-center gap-1.5 rounded-md bg-background " \
|
|
8
|
+
TRIGGER_CLS = "inline-flex h-9 w-max items-center justify-center gap-1.5 rounded-md bg-background " \
|
|
9
9
|
"px-4 py-2 text-sm font-medium transition-[color,box-shadow] outline-none " \
|
|
10
10
|
"hover:bg-accent hover:text-accent-foreground " \
|
|
11
|
-
"
|
|
12
|
-
"data-[state=open]:bg-accent/50 data-[state=open]:text-accent-foreground"
|
|
11
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
12
|
+
"data-[state=open]:bg-accent/50 data-[state=open]:text-accent-foreground " \
|
|
13
|
+
"data-[state=open]:hover:bg-accent"
|
|
13
14
|
|
|
14
|
-
PANEL_CLS = "
|
|
15
|
-
"bg-popover text-popover-foreground shadow-lg"
|
|
15
|
+
PANEL_CLS = "#{UI::Styles::POPOVER_PANEL} left-0 top-full mt-2 w-full overflow-hidden"
|
|
16
16
|
|
|
17
17
|
INNER_CLS = "container mx-auto grid gap-6 p-6"
|
|
18
18
|
|
|
19
19
|
COLUMN_HEADING = "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground"
|
|
20
20
|
|
|
21
|
-
ITEM_CLS = "group flex items-start gap-3 rounded-
|
|
21
|
+
ITEM_CLS = "group flex items-start gap-3 rounded-md p-2 text-sm transition-all outline-none " \
|
|
22
22
|
"hover:bg-accent hover:text-accent-foreground " \
|
|
23
|
-
"
|
|
23
|
+
"#{UI::Styles::FOCUS_RING}"
|
|
24
24
|
|
|
25
25
|
ITEM_TITLE = "font-medium leading-none"
|
|
26
|
-
ITEM_DESC = "mt-1 text-xs
|
|
26
|
+
ITEM_DESC = "mt-1 text-xs leading-normal text-muted-foreground"
|
|
27
27
|
|
|
28
28
|
CHEVRON_PATH = "m6 9 6 6 6-6"
|
|
29
29
|
|
|
@@ -87,7 +87,7 @@ module UI
|
|
|
87
87
|
fill: "none",
|
|
88
88
|
stroke: "currentColor",
|
|
89
89
|
"stroke-width": "2",
|
|
90
|
-
class: "size-
|
|
90
|
+
class: "size-4 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180",
|
|
91
91
|
"aria-hidden": "true",
|
|
92
92
|
data: { mega_menu_target: "chevron" })
|
|
93
93
|
end
|
|
@@ -4,13 +4,13 @@ module UI
|
|
|
4
4
|
class MenubarComponent < ApplicationComponent
|
|
5
5
|
renders_many :menus, "UI::MenubarMenuComponent"
|
|
6
6
|
|
|
7
|
-
BAR = "flex h-9 items-center gap-1 rounded-md
|
|
8
|
-
ITEM = "
|
|
9
|
-
"
|
|
10
|
-
"
|
|
7
|
+
BAR = "flex h-9 items-center gap-1 rounded-md #{UI::Styles::BORDER} bg-background p-1 shadow-xs"
|
|
8
|
+
ITEM = "#{UI::Styles::MENU_ITEM} w-full whitespace-nowrap hover:bg-accent hover:text-accent-foreground " \
|
|
9
|
+
"data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 " \
|
|
10
|
+
"dark:data-[variant=destructive]:focus:bg-destructive/20 " \
|
|
11
11
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 " \
|
|
12
12
|
"[&_svg:not([class*='text-'])]:text-muted-foreground"
|
|
13
|
-
SEPARATOR =
|
|
13
|
+
SEPARATOR = UI::Styles::MENU_SEPARATOR
|
|
14
14
|
LABEL_CLS = "px-2 py-1.5 text-sm font-medium"
|
|
15
15
|
|
|
16
16
|
def initialize(**html_attrs)
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class MenubarMenuComponent < ApplicationComponent
|
|
5
|
-
TRIGGER = "flex
|
|
6
|
-
"
|
|
7
|
-
"
|
|
5
|
+
TRIGGER = "flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none " \
|
|
6
|
+
"focus:bg-accent focus:text-accent-foreground " \
|
|
7
|
+
"data-[state=open]:bg-accent data-[state=open]:text-accent-foreground"
|
|
8
8
|
|
|
9
|
-
PANEL = "
|
|
10
|
-
"bg-popover p-1 text-popover-foreground shadow-md"
|
|
9
|
+
PANEL = "#{UI::Styles::POPOVER_PANEL} left-0 top-full z-50 mt-1 w-max min-w-[12rem] overflow-hidden p-1"
|
|
11
10
|
|
|
12
11
|
def initialize(label:, **html_attrs)
|
|
13
12
|
@label = label
|
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class NavbarComponent < ApplicationComponent
|
|
5
|
-
LINK_BASE = "text-sm font-medium transition-
|
|
5
|
+
LINK_BASE = "inline-flex h-9 items-center rounded-md px-3 text-sm font-medium transition-[color,box-shadow] " \
|
|
6
|
+
"outline-none hover:bg-accent hover:text-accent-foreground " \
|
|
7
|
+
"#{UI::Styles::FOCUS_RING}"
|
|
6
8
|
LINK_IDLE = "text-muted-foreground"
|
|
7
|
-
LINK_ACTIVE = "text-foreground"
|
|
9
|
+
LINK_ACTIVE = "bg-accent/50 text-accent-foreground"
|
|
10
|
+
MOBILE_LINK = "block rounded-md px-3 py-2 text-sm font-medium transition-colors outline-none " \
|
|
11
|
+
"hover:bg-accent hover:text-accent-foreground #{UI::Styles::FOCUS_RING}"
|
|
8
12
|
|
|
9
13
|
# items: [{ label:, href:, active: (optional) }]
|
|
10
14
|
# Block content is placed in the right action area (e.g. a Sign in button).
|
|
@@ -18,21 +22,27 @@ module UI
|
|
|
18
22
|
|
|
19
23
|
def call
|
|
20
24
|
content_tag(:nav,
|
|
21
|
-
class: cn("sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/
|
|
25
|
+
class: cn("sticky top-0 z-50 w-full border-b border-border bg-background/95 shadow-xs backdrop-blur supports-[backdrop-filter]:bg-background/80", @extra_class),
|
|
26
|
+
"aria-label": "Main navigation",
|
|
22
27
|
data: { controller: "navbar" },
|
|
23
28
|
**@html_attrs) do
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
concat desktop_menu
|
|
27
|
-
concat spacer
|
|
28
|
-
concat action_area
|
|
29
|
-
concat hamburger
|
|
30
|
-
end
|
|
29
|
+
concat header_bar
|
|
30
|
+
concat mobile_menu if @items.any?
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
+
def header_bar
|
|
37
|
+
content_tag(:div, class: "container mx-auto flex h-14 items-center gap-4 px-4") do
|
|
38
|
+
concat brand_link
|
|
39
|
+
concat desktop_menu
|
|
40
|
+
concat spacer
|
|
41
|
+
concat action_area
|
|
42
|
+
concat hamburger
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
36
46
|
def brand_link
|
|
37
47
|
return "" unless @brand
|
|
38
48
|
|
|
@@ -70,13 +80,43 @@ module UI
|
|
|
70
80
|
|
|
71
81
|
content_tag(:button, nil,
|
|
72
82
|
type: "button",
|
|
73
|
-
class: "
|
|
83
|
+
class: "inline-flex size-9 items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-accent hover:text-accent-foreground #{UI::Styles::FOCUS_RING} md:hidden",
|
|
74
84
|
data: { action: "click->navbar#toggle", navbar_target: "toggle" },
|
|
85
|
+
"aria-expanded": "false",
|
|
86
|
+
"aria-controls": mobile_menu_id,
|
|
75
87
|
"aria-label": "Toggle menu") do
|
|
76
88
|
hamburger_icon
|
|
77
89
|
end
|
|
78
90
|
end
|
|
79
91
|
|
|
92
|
+
def mobile_menu
|
|
93
|
+
content_tag(:div,
|
|
94
|
+
id: mobile_menu_id,
|
|
95
|
+
class: "md:hidden border-b border-border bg-background px-4 py-3",
|
|
96
|
+
data: { navbar_target: "menu" },
|
|
97
|
+
hidden: true) do
|
|
98
|
+
concat content_tag(:div, class: "flex flex-col gap-1") {
|
|
99
|
+
safe_join(@items.map { |item| mobile_nav_link(item) })
|
|
100
|
+
}
|
|
101
|
+
concat mobile_action_area if content?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def mobile_nav_link(item)
|
|
106
|
+
content_tag(:a, item[:label],
|
|
107
|
+
href: item[:href],
|
|
108
|
+
class: cn(MOBILE_LINK, item[:active] ? LINK_ACTIVE : LINK_IDLE),
|
|
109
|
+
data: { action: "click->navbar#close" })
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def mobile_action_area
|
|
113
|
+
content_tag(:div, content, class: "mt-3 flex flex-col gap-2 border-t border-border pt-3")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def mobile_menu_id
|
|
117
|
+
@mobile_menu_id ||= "navbar-menu-#{object_id}"
|
|
118
|
+
end
|
|
119
|
+
|
|
80
120
|
def hamburger_icon
|
|
81
121
|
raw(<<~SVG)
|
|
82
122
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
@@ -4,8 +4,13 @@ export default class extends Controller {
|
|
|
4
4
|
static targets = ["menu", "toggle"]
|
|
5
5
|
|
|
6
6
|
toggle() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const open = this.menuTarget.hidden
|
|
8
|
+
this.menuTarget.hidden = !open
|
|
9
|
+
this.toggleTarget.setAttribute("aria-expanded", open ? "true" : "false")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
close() {
|
|
13
|
+
this.menuTarget.hidden = true
|
|
14
|
+
this.toggleTarget.setAttribute("aria-expanded", "false")
|
|
10
15
|
}
|
|
11
16
|
}
|
data/lib/generators/view_primitives/add/templates/navigation_menu/navigation_menu_component.rb.tt
CHANGED
|
@@ -2,32 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class NavigationMenuComponent < ApplicationComponent
|
|
5
|
-
ROOT = "relative flex max-w-max flex-1 items-center justify-center"
|
|
6
|
-
LIST = "flex flex-1 list-none items-center justify-center gap-1"
|
|
5
|
+
ROOT = "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center"
|
|
6
|
+
LIST = "group flex flex-1 list-none items-center justify-center gap-1"
|
|
7
7
|
|
|
8
|
-
# Trigger button style (item with flyout content)
|
|
9
8
|
TRIGGER = "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background " \
|
|
10
9
|
"px-4 py-2 text-sm font-medium transition-[color,box-shadow] outline-none " \
|
|
11
|
-
"hover:bg-accent hover:text-accent-foreground " \
|
|
12
|
-
"
|
|
10
|
+
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground " \
|
|
11
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
13
12
|
"disabled:pointer-events-none disabled:opacity-50 " \
|
|
14
|
-
"data-[state=open]:bg-accent/50 data-[state=open]:text-accent-foreground"
|
|
13
|
+
"data-[state=open]:bg-accent/50 data-[state=open]:text-accent-foreground " \
|
|
14
|
+
"data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
|
15
15
|
|
|
16
|
-
# Plain link style (item without flyout)
|
|
17
16
|
LINK_CLS = "inline-flex h-9 w-max items-center justify-center rounded-md bg-background " \
|
|
18
17
|
"px-4 py-2 text-sm font-medium transition-[color,box-shadow] outline-none " \
|
|
19
|
-
"hover:bg-accent hover:text-accent-foreground " \
|
|
20
|
-
"
|
|
18
|
+
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground " \
|
|
19
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
21
20
|
"aria-[current]:bg-accent/50 aria-[current]:text-accent-foreground"
|
|
22
21
|
|
|
23
|
-
#
|
|
24
|
-
CONTENT = "absolute top-full left-0 z-50 mt-1.5 min-w-48 overflow-hidden rounded-md border " \
|
|
25
|
-
"bg-popover p-1 text-popover-foreground shadow"
|
|
22
|
+
CONTENT = "#{UI::Styles::POPOVER_PANEL} top-full left-0 mt-1.5 min-w-48 overflow-hidden p-2 shadow"
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"focus-visible:ring-[3px] focus-visible:ring-ring/50 " \
|
|
24
|
+
PANEL_LINK = "flex flex-col gap-1 rounded-md p-2 text-sm transition-all outline-none " \
|
|
25
|
+
"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground " \
|
|
26
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
31
27
|
"aria-[current]:bg-accent/50 aria-[current]:text-accent-foreground"
|
|
32
28
|
|
|
33
29
|
CHEVRON_PATH = "m6 9 6 6 6-6"
|
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class NumberInputComponent < ApplicationComponent
|
|
5
|
-
|
|
6
|
-
|
|
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:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " \
|
|
11
|
-
"[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none " \
|
|
12
|
-
"md:text-sm dark:bg-input/30"
|
|
5
|
+
SPINNER_HIDE = "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none " \
|
|
6
|
+
"[&::-webkit-outer-spin-button]:appearance-none"
|
|
13
7
|
|
|
14
8
|
# min / max / step: native number input attributes
|
|
15
9
|
# value: initial value
|
|
@@ -18,12 +12,11 @@ module UI
|
|
|
18
12
|
@max = max
|
|
19
13
|
@step = step
|
|
20
14
|
@value = value
|
|
21
|
-
|
|
22
|
-
@html_attrs = html_attrs
|
|
15
|
+
extract_html_attrs(**html_attrs)
|
|
23
16
|
end
|
|
24
17
|
|
|
25
18
|
def call
|
|
26
|
-
attrs = { type: "number", class: cn(
|
|
19
|
+
attrs = { type: "number", class: cn(UI::Styles::INPUT, SPINNER_HIDE, @extra_class) }
|
|
27
20
|
attrs[:min] = @min unless @min.nil?
|
|
28
21
|
attrs[:max] = @max unless @max.nil?
|
|
29
22
|
attrs[:step] = @step unless @step.nil?
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class PaginationComponent < ApplicationComponent
|
|
5
|
-
ITEM = "inline-flex items-center justify-center whitespace-nowrap rounded-md
|
|
6
|
-
"
|
|
7
|
-
"
|
|
5
|
+
ITEM = "inline-flex h-9 w-9 items-center justify-center whitespace-nowrap rounded-md border border-input " \
|
|
6
|
+
"bg-background text-sm font-medium shadow-xs transition-colors outline-none " \
|
|
7
|
+
"hover:bg-accent hover:text-accent-foreground " \
|
|
8
|
+
"#{UI::Styles::FOCUS_RING}"
|
|
8
9
|
ACTIVE = "bg-primary text-primary-foreground shadow hover:bg-primary/90 border-transparent"
|
|
9
10
|
MUTED = "cursor-not-allowed opacity-50 pointer-events-none"
|
|
10
11
|
|
|
@@ -54,7 +54,8 @@ module UI
|
|
|
54
54
|
private
|
|
55
55
|
|
|
56
56
|
def fallback_img
|
|
57
|
-
attrs = { src: @src, alt: @alt, loading: @loading,
|
|
57
|
+
attrs = { src: @src, alt: @alt, loading: @loading,
|
|
58
|
+
class: cn("h-auto w-full max-w-full rounded-md", @extra_class) }
|
|
58
59
|
attrs[:width] = @width if @width
|
|
59
60
|
attrs[:height] = @height if @height
|
|
60
61
|
tag.img(**attrs)
|
|
@@ -4,8 +4,7 @@ module UI
|
|
|
4
4
|
class PopoverComponent < ApplicationComponent
|
|
5
5
|
renders_one :trigger
|
|
6
6
|
|
|
7
|
-
PANEL_BASE = "
|
|
8
|
-
"text-sm text-popover-foreground shadow-md outline-none"
|
|
7
|
+
PANEL_BASE = "#{UI::Styles::POPOVER_PANEL} w-72 p-4"
|
|
9
8
|
|
|
10
9
|
ALIGN = {
|
|
11
10
|
start: "left-0",
|
|
@@ -16,12 +16,14 @@ module UI
|
|
|
16
16
|
def call
|
|
17
17
|
content_tag(:div,
|
|
18
18
|
class: cn(TRACK, @extra_class),
|
|
19
|
+
data: { slot: "progress" },
|
|
19
20
|
role: "progressbar",
|
|
20
21
|
"aria-valuenow": @value,
|
|
21
22
|
"aria-valuemin": 0,
|
|
22
23
|
"aria-valuemax": @max,
|
|
23
24
|
**@html_attrs) do
|
|
24
|
-
content_tag(:div, nil, class: BAR, style: "width: #{@pct.round(2)}%"
|
|
25
|
+
content_tag(:div, nil, class: BAR, style: "width: #{@pct.round(2)}%",
|
|
26
|
+
data: { slot: "progress-indicator" })
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
end
|
|
@@ -12,7 +12,7 @@ module UI
|
|
|
12
12
|
# <%%= RQRCode::QRCode.new("https://example.com").as_svg(viewbox: true).html_safe %>
|
|
13
13
|
# <%% end %>
|
|
14
14
|
|
|
15
|
-
WRAPPER_CLS = "inline-flex items-center justify-center overflow-hidden rounded-
|
|
15
|
+
WRAPPER_CLS = "inline-flex items-center justify-center overflow-hidden rounded-md #{UI::Styles::BORDER} bg-card p-4 shadow-xs"
|
|
16
16
|
|
|
17
17
|
# src: image URL for a pre-rendered QR (renders an <img>)
|
|
18
18
|
# alt: accessible label for the <img> (default: "QR code")
|
|
@@ -12,7 +12,7 @@ module UI
|
|
|
12
12
|
|
|
13
13
|
def call
|
|
14
14
|
content_tag(:div,
|
|
15
|
-
class: cn("grid gap-
|
|
15
|
+
class: cn("grid gap-3", @extra_class),
|
|
16
16
|
role: "radiogroup",
|
|
17
17
|
**@html_attrs) do
|
|
18
18
|
if @items.any?
|
|
@@ -35,9 +35,12 @@ module UI
|
|
|
35
35
|
|
|
36
36
|
def radio_input(item, id)
|
|
37
37
|
attrs = { type: "radio", name: @name, value: item[:value], id: id,
|
|
38
|
-
class: "
|
|
39
|
-
"
|
|
40
|
-
"
|
|
38
|
+
class: "aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs " \
|
|
39
|
+
"transition-[color,box-shadow] outline-none accent-primary " \
|
|
40
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
41
|
+
"disabled:cursor-not-allowed disabled:opacity-50 " \
|
|
42
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 " \
|
|
43
|
+
"dark:bg-input/30 dark:aria-invalid:ring-destructive/40" }
|
|
41
44
|
attrs[:checked] = true if item[:checked]
|
|
42
45
|
content_tag(:input, nil, **attrs)
|
|
43
46
|
end
|
|
@@ -45,7 +48,7 @@ module UI
|
|
|
45
48
|
def radio_label(item, id)
|
|
46
49
|
content_tag(:label, item[:label],
|
|
47
50
|
for: id,
|
|
48
|
-
class: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-
|
|
51
|
+
class: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50")
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
end
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module UI
|
|
4
4
|
class RangeComponent < ApplicationComponent
|
|
5
|
-
BASE = "w-full cursor-pointer appearance-none rounded-full bg-
|
|
6
|
-
"
|
|
7
|
-
"focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 " \
|
|
5
|
+
BASE = "h-2 w-full cursor-pointer appearance-none rounded-full bg-muted outline-none accent-primary " \
|
|
6
|
+
"#{UI::Styles::FOCUS_RING} " \
|
|
8
7
|
"disabled:pointer-events-none disabled:opacity-50 " \
|
|
9
8
|
"[&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none " \
|
|
10
9
|
"[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary " \
|
|
@@ -30,7 +30,7 @@ module UI
|
|
|
30
30
|
def star(filled)
|
|
31
31
|
content_tag(:svg,
|
|
32
32
|
content_tag(:path, nil, d: STAR_PATH, "stroke-linecap": "round", "stroke-linejoin": "round"),
|
|
33
|
-
class: filled ? "size-
|
|
33
|
+
class: filled ? "size-4 fill-primary text-primary" : "size-4 fill-muted text-muted",
|
|
34
34
|
xmlns: "http://www.w3.org/2000/svg",
|
|
35
35
|
viewBox: "0 0 24 24",
|
|
36
36
|
fill: filled ? "currentColor" : "none",
|
|
@@ -21,7 +21,7 @@ export default class extends Controller {
|
|
|
21
21
|
#render(upTo) {
|
|
22
22
|
this.starTargets.forEach((star, i) => {
|
|
23
23
|
const filled = i < upTo
|
|
24
|
-
star.classList.toggle("text-
|
|
24
|
+
star.classList.toggle("text-primary", filled)
|
|
25
25
|
star.classList.toggle("text-muted-foreground", !filled)
|
|
26
26
|
const svg = star.querySelector("svg")
|
|
27
27
|
if (svg) svg.setAttribute("fill", filled ? "currentColor" : "none")
|
|
@@ -45,8 +45,9 @@ module UI
|
|
|
45
45
|
star_svg(filled),
|
|
46
46
|
type: "button",
|
|
47
47
|
class: cn(
|
|
48
|
-
"transition-colors
|
|
49
|
-
|
|
48
|
+
"rounded-sm transition-colors outline-none",
|
|
49
|
+
UI::Styles::FOCUS_RING,
|
|
50
|
+
filled ? "text-primary" : "text-muted-foreground"
|
|
50
51
|
),
|
|
51
52
|
data: {
|
|
52
53
|
rating_target: "star",
|
|
@@ -59,7 +60,7 @@ module UI
|
|
|
59
60
|
def star_svg(filled)
|
|
60
61
|
content_tag(:svg,
|
|
61
62
|
content_tag(:path, nil, d: STAR_PATH, "stroke-linecap": "round", "stroke-linejoin": "round"),
|
|
62
|
-
class: "size-
|
|
63
|
+
class: "size-5 pointer-events-none",
|
|
63
64
|
xmlns: "http://www.w3.org/2000/svg",
|
|
64
65
|
viewBox: "0 0 24 24",
|
|
65
66
|
fill: filled ? "currentColor" : "none",
|