shadcn-phlex 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.
Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +195 -0
  3. data/app.css +20 -0
  4. data/css/shadcn-source.css +3 -0
  5. data/css/shadcn-tailwind.css +160 -0
  6. data/css/themes/mauve.css +62 -0
  7. data/css/themes/mist.css +62 -0
  8. data/css/themes/neutral.css +74 -0
  9. data/css/themes/olive.css +62 -0
  10. data/css/themes/stone.css +62 -0
  11. data/css/themes/taupe.css +62 -0
  12. data/css/themes/zinc.css +62 -0
  13. data/js/controllers/accordion_controller.js +135 -0
  14. data/js/controllers/checkbox_controller.js +52 -0
  15. data/js/controllers/collapsible_controller.js +85 -0
  16. data/js/controllers/combobox_controller.js +168 -0
  17. data/js/controllers/command_controller.js +171 -0
  18. data/js/controllers/context_menu_controller.js +132 -0
  19. data/js/controllers/dark_mode_controller.js +106 -0
  20. data/js/controllers/dialog_controller.js +205 -0
  21. data/js/controllers/drawer_controller.js +161 -0
  22. data/js/controllers/dropdown_menu_controller.js +189 -0
  23. data/js/controllers/hover_card_controller.js +85 -0
  24. data/js/controllers/index.js +89 -0
  25. data/js/controllers/menubar_controller.js +171 -0
  26. data/js/controllers/navigation_menu_controller.js +160 -0
  27. data/js/controllers/popover_controller.js +151 -0
  28. data/js/controllers/radio_group_controller.js +78 -0
  29. data/js/controllers/scroll_area_controller.js +117 -0
  30. data/js/controllers/select_controller.js +198 -0
  31. data/js/controllers/sheet_controller.js +130 -0
  32. data/js/controllers/slider_controller.js +142 -0
  33. data/js/controllers/switch_controller.js +40 -0
  34. data/js/controllers/tabs_controller.js +96 -0
  35. data/js/controllers/toast_controller.js +206 -0
  36. data/js/controllers/toggle_controller.js +30 -0
  37. data/js/controllers/toggle_group_controller.js +73 -0
  38. data/js/controllers/tooltip_controller.js +146 -0
  39. data/lib/generators/shadcn_phlex/component_generator.rb +79 -0
  40. data/lib/generators/shadcn_phlex/install_generator.rb +217 -0
  41. data/lib/shadcn/base.rb +27 -0
  42. data/lib/shadcn/engine.rb +24 -0
  43. data/lib/shadcn/kit.rb +1158 -0
  44. data/lib/shadcn/themes/accent_colors.rb +106 -0
  45. data/lib/shadcn/themes/base_colors.rb +313 -0
  46. data/lib/shadcn/ui/accordion.rb +135 -0
  47. data/lib/shadcn/ui/alert.rb +79 -0
  48. data/lib/shadcn/ui/alert_dialog.rb +220 -0
  49. data/lib/shadcn/ui/aspect_ratio.rb +35 -0
  50. data/lib/shadcn/ui/avatar.rb +134 -0
  51. data/lib/shadcn/ui/badge.rb +48 -0
  52. data/lib/shadcn/ui/breadcrumb.rb +180 -0
  53. data/lib/shadcn/ui/button.rb +63 -0
  54. data/lib/shadcn/ui/button_group.rb +58 -0
  55. data/lib/shadcn/ui/card.rb +133 -0
  56. data/lib/shadcn/ui/checkbox.rb +72 -0
  57. data/lib/shadcn/ui/collapsible.rb +76 -0
  58. data/lib/shadcn/ui/combobox.rb +229 -0
  59. data/lib/shadcn/ui/command.rb +256 -0
  60. data/lib/shadcn/ui/context_menu.rb +319 -0
  61. data/lib/shadcn/ui/dialog.rb +226 -0
  62. data/lib/shadcn/ui/direction.rb +23 -0
  63. data/lib/shadcn/ui/drawer.rb +217 -0
  64. data/lib/shadcn/ui/dropdown_menu.rb +384 -0
  65. data/lib/shadcn/ui/empty.rb +97 -0
  66. data/lib/shadcn/ui/field.rb +126 -0
  67. data/lib/shadcn/ui/hover_card.rb +75 -0
  68. data/lib/shadcn/ui/input.rb +36 -0
  69. data/lib/shadcn/ui/input_group.rb +32 -0
  70. data/lib/shadcn/ui/input_otp.rb +112 -0
  71. data/lib/shadcn/ui/item.rb +115 -0
  72. data/lib/shadcn/ui/kbd.rb +45 -0
  73. data/lib/shadcn/ui/label.rb +28 -0
  74. data/lib/shadcn/ui/menubar.rb +345 -0
  75. data/lib/shadcn/ui/native_select.rb +31 -0
  76. data/lib/shadcn/ui/navigation_menu.rb +238 -0
  77. data/lib/shadcn/ui/pagination.rb +224 -0
  78. data/lib/shadcn/ui/popover.rb +147 -0
  79. data/lib/shadcn/ui/progress.rb +40 -0
  80. data/lib/shadcn/ui/radio_group.rb +92 -0
  81. data/lib/shadcn/ui/resizable.rb +108 -0
  82. data/lib/shadcn/ui/scroll_area.rb +75 -0
  83. data/lib/shadcn/ui/select.rb +235 -0
  84. data/lib/shadcn/ui/separator.rb +36 -0
  85. data/lib/shadcn/ui/sheet.rb +231 -0
  86. data/lib/shadcn/ui/sidebar.rb +420 -0
  87. data/lib/shadcn/ui/skeleton.rb +23 -0
  88. data/lib/shadcn/ui/slider.rb +72 -0
  89. data/lib/shadcn/ui/sonner.rb +177 -0
  90. data/lib/shadcn/ui/spinner.rb +58 -0
  91. data/lib/shadcn/ui/switch.rb +75 -0
  92. data/lib/shadcn/ui/table.rb +154 -0
  93. data/lib/shadcn/ui/tabs.rb +154 -0
  94. data/lib/shadcn/ui/text_field.rb +146 -0
  95. data/lib/shadcn/ui/textarea.rb +32 -0
  96. data/lib/shadcn/ui/theme_toggle.rb +74 -0
  97. data/lib/shadcn/ui/toggle.rb +66 -0
  98. data/lib/shadcn/ui/toggle_group.rb +75 -0
  99. data/lib/shadcn/ui/tooltip.rb +78 -0
  100. data/lib/shadcn/ui/typography.rb +217 -0
  101. data/lib/shadcn/version.rb +5 -0
  102. data/lib/shadcn-phlex.rb +6 -0
  103. data/lib/shadcn.rb +80 -0
  104. data/package.json +14 -0
  105. data/skills/shadcn-phlex/SKILL.md +190 -0
  106. data/skills/shadcn-phlex/evals/evals.json +90 -0
  107. data/skills/shadcn-phlex/references/component-catalog.md +355 -0
  108. data/skills/shadcn-phlex/rules/composition.md +235 -0
  109. data/skills/shadcn-phlex/rules/forms.md +151 -0
  110. data/skills/shadcn-phlex/rules/helpers.md +54 -0
  111. data/skills/shadcn-phlex/rules/stimulus.md +61 -0
  112. data/skills/shadcn-phlex/rules/styling.md +177 -0
  113. metadata +209 -0
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui AlertDialog
6
+ class AlertDialog < Base
7
+ def initialize(**attrs)
8
+ @attrs = attrs
9
+ end
10
+
11
+ def view_template(&block)
12
+ div(**build_attrs, &block)
13
+ end
14
+
15
+ private
16
+
17
+ def build_attrs
18
+ @attrs.merge(
19
+ data_slot: "alert-dialog",
20
+ data_controller: "shadcn--dialog",
21
+ data_shadcn__dialog_close_on_overlay_value: false
22
+ )
23
+ end
24
+ end
25
+
26
+ class AlertDialogTrigger < Base
27
+ def initialize(**attrs)
28
+ @attrs = attrs
29
+ end
30
+
31
+ def view_template(&block)
32
+ div(**build_attrs, &block)
33
+ end
34
+
35
+ private
36
+
37
+ def build_attrs
38
+ @attrs.merge(
39
+ data_slot: "alert-dialog-trigger",
40
+ data_shadcn__dialog_target: "trigger",
41
+ data_action: "click->shadcn--dialog#show",
42
+ role: "button",
43
+ style: "display: inline-block"
44
+ )
45
+ end
46
+ end
47
+
48
+ class AlertDialogOverlay < Base
49
+ def initialize(**attrs)
50
+ @attrs = attrs
51
+ end
52
+
53
+ def view_template
54
+ div(**build_attrs)
55
+ end
56
+
57
+ private
58
+
59
+ def build_attrs
60
+ classes = cn(
61
+ "fixed inset-0 z-50 bg-black/50",
62
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
63
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0",
64
+ @attrs.delete(:class)
65
+ )
66
+ @attrs.merge(
67
+ data_slot: "alert-dialog-overlay",
68
+ data_shadcn__dialog_target: "overlay",
69
+ hidden: true,
70
+ class: classes
71
+ )
72
+ end
73
+ end
74
+
75
+ class AlertDialogContent < Base
76
+ def initialize(size: :default, **attrs)
77
+ @size = size
78
+ @attrs = attrs
79
+ end
80
+
81
+ def view_template(&block)
82
+ render AlertDialogOverlay.new
83
+
84
+ div(**build_attrs, &block)
85
+ end
86
+
87
+ private
88
+
89
+ def build_attrs
90
+ classes = cn(
91
+ "group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)]",
92
+ "translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200",
93
+ "data-[size=sm]:max-w-xs",
94
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
95
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
96
+ "data-[size=default]:sm:max-w-lg",
97
+ @attrs.delete(:class)
98
+ )
99
+ @attrs.merge(
100
+ data_slot: "alert-dialog-content",
101
+ data_shadcn__dialog_target: "content",
102
+ data_size: @size,
103
+ role: "alertdialog",
104
+ hidden: true,
105
+ class: classes
106
+ )
107
+ end
108
+ end
109
+
110
+ class AlertDialogHeader < Base
111
+ def initialize(**attrs)
112
+ @attrs = attrs
113
+ end
114
+
115
+ def view_template(&block)
116
+ div(**build_attrs, &block)
117
+ end
118
+
119
+ private
120
+
121
+ def build_attrs
122
+ classes = cn(
123
+ "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center",
124
+ "sm:group-data-[size=default]/alert-dialog-content:place-items-start",
125
+ "sm:group-data-[size=default]/alert-dialog-content:text-left",
126
+ @attrs.delete(:class)
127
+ )
128
+ @attrs.merge(data_slot: "alert-dialog-header", class: classes)
129
+ end
130
+ end
131
+
132
+ class AlertDialogFooter < Base
133
+ def initialize(**attrs)
134
+ @attrs = attrs
135
+ end
136
+
137
+ def view_template(&block)
138
+ div(**build_attrs, &block)
139
+ end
140
+
141
+ private
142
+
143
+ def build_attrs
144
+ classes = cn(
145
+ "flex flex-col-reverse gap-2",
146
+ "group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2",
147
+ "sm:flex-row sm:justify-end",
148
+ @attrs.delete(:class)
149
+ )
150
+ @attrs.merge(data_slot: "alert-dialog-footer", class: classes)
151
+ end
152
+ end
153
+
154
+ class AlertDialogTitle < Base
155
+ def initialize(**attrs)
156
+ @attrs = attrs
157
+ end
158
+
159
+ def view_template(&block)
160
+ h2(**build_attrs, &block)
161
+ end
162
+
163
+ private
164
+
165
+ def build_attrs
166
+ classes = cn("text-lg font-semibold", @attrs.delete(:class))
167
+ @attrs.merge(
168
+ data_slot: "alert-dialog-title",
169
+ data_shadcn__dialog_target: "title",
170
+ class: classes
171
+ )
172
+ end
173
+ end
174
+
175
+ class AlertDialogDescription < Base
176
+ def initialize(**attrs)
177
+ @attrs = attrs
178
+ end
179
+
180
+ def view_template(&block)
181
+ p(**build_attrs, &block)
182
+ end
183
+
184
+ private
185
+
186
+ def build_attrs
187
+ classes = cn("text-sm text-muted-foreground", @attrs.delete(:class))
188
+ @attrs.merge(
189
+ data_slot: "alert-dialog-description",
190
+ data_shadcn__dialog_target: "description",
191
+ class: classes
192
+ )
193
+ end
194
+ end
195
+
196
+ class AlertDialogAction < Base
197
+ def initialize(variant: :default, size: :default, **attrs)
198
+ @variant = variant
199
+ @size = size
200
+ @attrs = attrs
201
+ end
202
+
203
+ def view_template(&block)
204
+ render Button.new(variant: @variant, size: @size, data_slot: "alert-dialog-action", **@attrs, &block)
205
+ end
206
+ end
207
+
208
+ class AlertDialogCancel < Base
209
+ def initialize(variant: :outline, size: :default, **attrs)
210
+ @variant = variant
211
+ @size = size
212
+ @attrs = attrs
213
+ end
214
+
215
+ def view_template(&block)
216
+ render Button.new(variant: @variant, size: @size, data_slot: "alert-dialog-cancel", data_action: "click->shadcn--dialog#hide", **@attrs, &block)
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui AspectRatio
6
+ # Uses the padding-bottom trick with absolute-positioned children
7
+ class AspectRatio < Base
8
+ def initialize(ratio: 1.0, **attrs)
9
+ @ratio = ratio
10
+ @attrs = attrs
11
+ end
12
+
13
+ def view_template(&block)
14
+ div(**build_attrs) do
15
+ div(style: "position: absolute; inset: 0;", &block)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def build_attrs
22
+ classes = cn("relative w-full", @attrs.delete(:class))
23
+ style = "position: relative; padding-bottom: #{(1.0 / @ratio * 100).round(4)}%"
24
+ existing_style = @attrs.delete(:style)
25
+ style = "#{existing_style}; #{style}" if existing_style
26
+
27
+ @attrs.merge(
28
+ data_slot: "aspect-ratio",
29
+ class: classes,
30
+ style: style
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Avatar
6
+ # Sizes: default, sm, lg
7
+ class Avatar < Base
8
+ def initialize(size: :default, **attrs)
9
+ @size = size
10
+ @attrs = attrs
11
+ end
12
+
13
+ def view_template(&block)
14
+ span(**build_attrs, &block)
15
+ end
16
+
17
+ private
18
+
19
+ def build_attrs
20
+ classes = cn(
21
+ "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none",
22
+ "data-[size=lg]:size-10 data-[size=sm]:size-6",
23
+ @attrs.delete(:class)
24
+ )
25
+ @attrs.merge(data_slot: "avatar", data_size: @size, class: classes)
26
+ end
27
+ end
28
+
29
+ class AvatarImage < Base
30
+ def initialize(src:, alt: "", **attrs)
31
+ @src = src
32
+ @alt = alt
33
+ @attrs = attrs
34
+ end
35
+
36
+ def view_template
37
+ img(**build_attrs)
38
+ end
39
+
40
+ private
41
+
42
+ def build_attrs
43
+ classes = cn("aspect-square size-full", @attrs.delete(:class))
44
+ @attrs.merge(data_slot: "avatar-image", src: @src, alt: @alt, class: classes)
45
+ end
46
+ end
47
+
48
+ class AvatarFallback < Base
49
+ def initialize(**attrs)
50
+ @attrs = attrs
51
+ end
52
+
53
+ def view_template(&block)
54
+ span(**build_attrs, &block)
55
+ end
56
+
57
+ private
58
+
59
+ def build_attrs
60
+ classes = cn(
61
+ "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground",
62
+ "group-data-[size=sm]/avatar:text-xs",
63
+ @attrs.delete(:class)
64
+ )
65
+ @attrs.merge(data_slot: "avatar-fallback", class: classes)
66
+ end
67
+ end
68
+
69
+ class AvatarBadge < Base
70
+ def initialize(**attrs)
71
+ @attrs = attrs
72
+ end
73
+
74
+ def view_template(&block)
75
+ span(**build_attrs, &block)
76
+ end
77
+
78
+ private
79
+
80
+ def build_attrs
81
+ classes = cn(
82
+ "absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full",
83
+ "bg-primary text-primary-foreground ring-2 ring-background select-none",
84
+ "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
85
+ "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
86
+ "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
87
+ @attrs.delete(:class)
88
+ )
89
+ @attrs.merge(data_slot: "avatar-badge", class: classes)
90
+ end
91
+ end
92
+
93
+ class AvatarGroup < Base
94
+ def initialize(**attrs)
95
+ @attrs = attrs
96
+ end
97
+
98
+ def view_template(&block)
99
+ div(**build_attrs, &block)
100
+ end
101
+
102
+ private
103
+
104
+ def build_attrs
105
+ classes = cn(
106
+ "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
107
+ @attrs.delete(:class)
108
+ )
109
+ @attrs.merge(data_slot: "avatar-group", class: classes)
110
+ end
111
+ end
112
+
113
+ class AvatarGroupCount < Base
114
+ def initialize(**attrs)
115
+ @attrs = attrs
116
+ end
117
+
118
+ def view_template(&block)
119
+ div(**build_attrs, &block)
120
+ end
121
+
122
+ private
123
+
124
+ def build_attrs
125
+ classes = cn(
126
+ "relative flex size-8 shrink-0 items-center justify-center rounded-full",
127
+ "bg-muted text-sm text-muted-foreground ring-2 ring-background",
128
+ @attrs.delete(:class)
129
+ )
130
+ @attrs.merge(data_slot: "avatar-group-count", class: classes)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Badge
6
+ # Variants: default, secondary, destructive, outline, ghost, link
7
+ class Badge < Base
8
+ VARIANTS = ClassVariants.build(
9
+ base: [
10
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap",
11
+ "transition-[color,box-shadow]",
12
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
13
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
14
+ "[&>svg]:pointer-events-none [&>svg]:size-3"
15
+ ].join(" "),
16
+ variants: {
17
+ variant: {
18
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
19
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
20
+ destructive: "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
21
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
22
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
23
+ link: "text-primary underline-offset-4 [a&]:hover:underline"
24
+ }
25
+ },
26
+ defaults: {
27
+ variant: :default
28
+ }
29
+ )
30
+
31
+ def initialize(variant: :default, **attrs)
32
+ @variant = variant
33
+ @attrs = attrs
34
+ end
35
+
36
+ def view_template(&block)
37
+ span(**build_attrs, &block)
38
+ end
39
+
40
+ private
41
+
42
+ def build_attrs
43
+ classes = cn(VARIANTS.render(variant: @variant), @attrs.delete(:class))
44
+ @attrs.merge(data_slot: "badge", class: classes)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Breadcrumb
6
+ class Breadcrumb < Base
7
+ def initialize(**attrs)
8
+ @attrs = attrs
9
+ end
10
+
11
+ def view_template(&block)
12
+ nav(**build_attrs, &block)
13
+ end
14
+
15
+ private
16
+
17
+ def build_attrs
18
+ @attrs.merge(data_slot: "breadcrumb", aria_label: "breadcrumb")
19
+ end
20
+ end
21
+
22
+ class BreadcrumbList < Base
23
+ def initialize(**attrs)
24
+ @attrs = attrs
25
+ end
26
+
27
+ def view_template(&block)
28
+ ol(**build_attrs, &block)
29
+ end
30
+
31
+ private
32
+
33
+ def build_attrs
34
+ classes = cn(
35
+ "flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
36
+ @attrs.delete(:class)
37
+ )
38
+ @attrs.merge(data_slot: "breadcrumb-list", class: classes)
39
+ end
40
+ end
41
+
42
+ class BreadcrumbItem < Base
43
+ def initialize(**attrs)
44
+ @attrs = attrs
45
+ end
46
+
47
+ def view_template(&block)
48
+ li(**build_attrs, &block)
49
+ end
50
+
51
+ private
52
+
53
+ def build_attrs
54
+ classes = cn("inline-flex items-center gap-1.5", @attrs.delete(:class))
55
+ @attrs.merge(data_slot: "breadcrumb-item", class: classes)
56
+ end
57
+ end
58
+
59
+ class BreadcrumbLink < Base
60
+ def initialize(href: "#", **attrs)
61
+ @href = href
62
+ @attrs = attrs
63
+ end
64
+
65
+ def view_template(&block)
66
+ a(**build_attrs, &block)
67
+ end
68
+
69
+ private
70
+
71
+ def build_attrs
72
+ classes = cn("transition-colors hover:text-foreground", @attrs.delete(:class))
73
+ @attrs.merge(data_slot: "breadcrumb-link", href: @href, class: classes)
74
+ end
75
+ end
76
+
77
+ class BreadcrumbPage < Base
78
+ def initialize(**attrs)
79
+ @attrs = attrs
80
+ end
81
+
82
+ def view_template(&block)
83
+ span(**build_attrs, &block)
84
+ end
85
+
86
+ private
87
+
88
+ def build_attrs
89
+ classes = cn("font-normal text-foreground", @attrs.delete(:class))
90
+ @attrs.merge(
91
+ data_slot: "breadcrumb-page",
92
+ role: "link",
93
+ aria_disabled: "true",
94
+ aria_current: "page",
95
+ class: classes
96
+ )
97
+ end
98
+ end
99
+
100
+ class BreadcrumbSeparator < Base
101
+ def initialize(**attrs)
102
+ @attrs = attrs
103
+ end
104
+
105
+ def view_template(&block)
106
+ li(**build_attrs) do
107
+ if block_given?
108
+ yield
109
+ else
110
+ # Default ChevronRight icon
111
+ svg(
112
+ xmlns: "http://www.w3.org/2000/svg",
113
+ width: "14", height: "14",
114
+ viewbox: "0 0 24 24",
115
+ fill: "none",
116
+ stroke: "currentColor",
117
+ stroke_width: "2",
118
+ stroke_linecap: "round",
119
+ stroke_linejoin: "round",
120
+ class: "size-3.5"
121
+ ) do |s|
122
+ s.path(d: "m9 18 6-6-6-6")
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def build_attrs
131
+ classes = cn("[&>svg]:size-3.5", @attrs.delete(:class))
132
+ @attrs.merge(
133
+ data_slot: "breadcrumb-separator",
134
+ role: "presentation",
135
+ aria_hidden: "true",
136
+ class: classes
137
+ )
138
+ end
139
+ end
140
+
141
+ class BreadcrumbEllipsis < Base
142
+ def initialize(**attrs)
143
+ @attrs = attrs
144
+ end
145
+
146
+ def view_template
147
+ span(**build_attrs) do
148
+ svg(
149
+ xmlns: "http://www.w3.org/2000/svg",
150
+ width: "16", height: "16",
151
+ viewbox: "0 0 24 24",
152
+ fill: "none",
153
+ stroke: "currentColor",
154
+ stroke_width: "2",
155
+ stroke_linecap: "round",
156
+ stroke_linejoin: "round",
157
+ class: "size-4"
158
+ ) do |s|
159
+ s.circle(cx: "12", cy: "12", r: "1")
160
+ s.circle(cx: "19", cy: "12", r: "1")
161
+ s.circle(cx: "5", cy: "12", r: "1")
162
+ end
163
+ span(class: "sr-only") { "More" }
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def build_attrs
170
+ classes = cn("flex size-9 items-center justify-center", @attrs.delete(:class))
171
+ @attrs.merge(
172
+ data_slot: "breadcrumb-ellipsis",
173
+ role: "presentation",
174
+ aria_hidden: "true",
175
+ class: classes
176
+ )
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Button
6
+ # Variants: default, destructive, outline, secondary, ghost, link
7
+ # Sizes: default, xs, sm, lg, icon, icon_xs, icon_sm, icon_lg
8
+ class Button < Base
9
+ VARIANTS = ClassVariants.build(
10
+ base: [
11
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap",
12
+ "transition-all outline-none",
13
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
14
+ "disabled:pointer-events-none disabled:opacity-50",
15
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
16
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
17
+ ].join(" "),
18
+ variants: {
19
+ variant: {
20
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
21
+ destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
22
+ outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
23
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
24
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
25
+ link: "text-primary underline-offset-4 hover:underline"
26
+ },
27
+ size: {
28
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
29
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
30
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
31
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
32
+ icon: "size-9",
33
+ icon_xs: "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
34
+ icon_sm: "size-8 rounded-md",
35
+ icon_lg: "size-10 rounded-md"
36
+ }
37
+ },
38
+ defaults: {
39
+ variant: :default,
40
+ size: :default
41
+ }
42
+ )
43
+
44
+ def initialize(variant: :default, size: :default, tag: :button, **attrs)
45
+ @variant = variant
46
+ @size = size
47
+ @tag = tag
48
+ @attrs = attrs
49
+ end
50
+
51
+ def view_template(&block)
52
+ send(@tag, **build_attrs, &block)
53
+ end
54
+
55
+ private
56
+
57
+ def build_attrs
58
+ classes = cn(VARIANTS.render(variant: @variant, size: @size), @attrs.delete(:class))
59
+ @attrs.merge(data_slot: "button", class: classes)
60
+ end
61
+ end
62
+ end
63
+ end