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,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui InputGroup
6
+ class InputGroup < 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
+ classes = cn(
19
+ "flex h-9 w-full items-center rounded-md border border-input bg-transparent shadow-xs",
20
+ "transition-[color,box-shadow]",
21
+ "focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50",
22
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
23
+ "dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
24
+ "has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50",
25
+ "[&>input]:border-0 [&>input]:bg-transparent [&>input]:shadow-none [&>input]:ring-0 [&>input]:focus-visible:ring-0 [&>input]:focus-visible:border-0",
26
+ @attrs.delete(:class)
27
+ )
28
+ @attrs.merge(data_slot: "input-group", class: classes)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui InputOTP
6
+ # One-time password input with individual digit boxes
7
+ class InputOTP < Base
8
+ def initialize(max_length: 6, **attrs)
9
+ @max_length = max_length
10
+ @attrs = attrs
11
+ end
12
+
13
+ def view_template(&block)
14
+ div(**build_attrs, &block)
15
+ end
16
+
17
+ private
18
+
19
+ def build_attrs
20
+ classes = cn(
21
+ "flex items-center gap-2 has-[:disabled]:opacity-50",
22
+ @attrs.delete(:class)
23
+ )
24
+ @attrs.merge(
25
+ data_slot: "input-otp",
26
+ data_input_otp_container: true,
27
+ class: classes
28
+ )
29
+ end
30
+ end
31
+
32
+ class InputOTPGroup < Base
33
+ def initialize(**attrs)
34
+ @attrs = attrs
35
+ end
36
+
37
+ def view_template(&block)
38
+ div(**build_attrs, &block)
39
+ end
40
+
41
+ private
42
+
43
+ def build_attrs
44
+ classes = cn("flex items-center", @attrs.delete(:class))
45
+ @attrs.merge(data_slot: "input-otp-group", class: classes)
46
+ end
47
+ end
48
+
49
+ class InputOTPSlot < Base
50
+ def initialize(index: 0, active: false, **attrs)
51
+ @index = index
52
+ @active = active
53
+ @attrs = attrs
54
+ end
55
+
56
+ def view_template(&block)
57
+ div(**build_attrs) do
58
+ if block_given?
59
+ yield
60
+ else
61
+ # Caret when active
62
+ if @active
63
+ div(class: "pointer-events-none absolute inset-0 flex items-center justify-center") do
64
+ div(class: "h-4 w-px animate-caret-blink bg-foreground duration-1000")
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def build_attrs
74
+ classes = cn(
75
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-xs transition-all",
76
+ "first:rounded-l-md first:border-l last:rounded-r-md",
77
+ @active ? "z-10 ring-2 ring-ring" : "",
78
+ @attrs.delete(:class)
79
+ )
80
+ @attrs.merge(
81
+ data_slot: "input-otp-slot",
82
+ data_index: @index,
83
+ data_active: @active,
84
+ class: classes
85
+ )
86
+ end
87
+ end
88
+
89
+ class InputOTPSeparator < Base
90
+ def initialize(**attrs)
91
+ @attrs = attrs
92
+ end
93
+
94
+ def view_template
95
+ div(**build_attrs) do
96
+ # Dot separator
97
+ svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16",
98
+ viewbox: "0 0 24 24", fill: "none", stroke: "currentColor",
99
+ stroke_width: "2", class: "size-4") do |s|
100
+ s.circle(cx: "12.1", cy: "12.1", r: "1")
101
+ end
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def build_attrs
108
+ @attrs.merge(data_slot: "input-otp-separator", role: "separator")
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Item
6
+ # Versatile content display component (media, content, actions)
7
+ class Item < Base
8
+ def initialize(**attrs)
9
+ @attrs = attrs
10
+ end
11
+
12
+ def view_template(&block)
13
+ div(**build_attrs, &block)
14
+ end
15
+
16
+ private
17
+
18
+ def build_attrs
19
+ classes = cn(
20
+ "flex items-start gap-3 rounded-lg p-3",
21
+ @attrs.delete(:class)
22
+ )
23
+ @attrs.merge(data_slot: "item", class: classes)
24
+ end
25
+ end
26
+
27
+ class ItemMedia < Base
28
+ def initialize(**attrs)
29
+ @attrs = attrs
30
+ end
31
+
32
+ def view_template(&block)
33
+ div(**build_attrs, &block)
34
+ end
35
+
36
+ private
37
+
38
+ def build_attrs
39
+ classes = cn(
40
+ "flex shrink-0 items-center justify-center [&_svg]:size-5 [&_svg]:text-muted-foreground",
41
+ @attrs.delete(:class)
42
+ )
43
+ @attrs.merge(data_slot: "item-media", class: classes)
44
+ end
45
+ end
46
+
47
+ class ItemContent < Base
48
+ def initialize(**attrs)
49
+ @attrs = attrs
50
+ end
51
+
52
+ def view_template(&block)
53
+ div(**build_attrs, &block)
54
+ end
55
+
56
+ private
57
+
58
+ def build_attrs
59
+ classes = cn("flex min-w-0 flex-1 flex-col gap-1", @attrs.delete(:class))
60
+ @attrs.merge(data_slot: "item-content", class: classes)
61
+ end
62
+ end
63
+
64
+ class ItemTitle < Base
65
+ def initialize(**attrs)
66
+ @attrs = attrs
67
+ end
68
+
69
+ def view_template(&block)
70
+ div(**build_attrs, &block)
71
+ end
72
+
73
+ private
74
+
75
+ def build_attrs
76
+ classes = cn("text-sm font-medium leading-none", @attrs.delete(:class))
77
+ @attrs.merge(data_slot: "item-title", class: classes)
78
+ end
79
+ end
80
+
81
+ class ItemDescription < Base
82
+ def initialize(**attrs)
83
+ @attrs = attrs
84
+ end
85
+
86
+ def view_template(&block)
87
+ p(**build_attrs, &block)
88
+ end
89
+
90
+ private
91
+
92
+ def build_attrs
93
+ classes = cn("text-sm text-muted-foreground", @attrs.delete(:class))
94
+ @attrs.merge(data_slot: "item-description", class: classes)
95
+ end
96
+ end
97
+
98
+ class ItemActions < Base
99
+ def initialize(**attrs)
100
+ @attrs = attrs
101
+ end
102
+
103
+ def view_template(&block)
104
+ div(**build_attrs, &block)
105
+ end
106
+
107
+ private
108
+
109
+ def build_attrs
110
+ classes = cn("flex shrink-0 items-center gap-1", @attrs.delete(:class))
111
+ @attrs.merge(data_slot: "item-actions", class: classes)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Kbd
6
+ class Kbd < Base
7
+ def initialize(**attrs)
8
+ @attrs = attrs
9
+ end
10
+
11
+ def view_template(&block)
12
+ kbd(**build_attrs, &block)
13
+ end
14
+
15
+ private
16
+
17
+ def build_attrs
18
+ classes = cn(
19
+ "pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1",
20
+ "rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none",
21
+ "[&_svg:not([class*='size-'])]:size-3",
22
+ @attrs.delete(:class)
23
+ )
24
+ @attrs.merge(data_slot: "kbd", class: classes)
25
+ end
26
+ end
27
+
28
+ class KbdGroup < Base
29
+ def initialize(**attrs)
30
+ @attrs = attrs
31
+ end
32
+
33
+ def view_template(&block)
34
+ kbd(**build_attrs, &block)
35
+ end
36
+
37
+ private
38
+
39
+ def build_attrs
40
+ classes = cn("inline-flex items-center gap-1", @attrs.delete(:class))
41
+ @attrs.merge(data_slot: "kbd-group", class: classes)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Label
6
+ class Label < Base
7
+ def initialize(**attrs)
8
+ @attrs = attrs
9
+ end
10
+
11
+ def view_template(&block)
12
+ label(**build_attrs, &block)
13
+ end
14
+
15
+ private
16
+
17
+ def build_attrs
18
+ classes = cn(
19
+ "flex items-center gap-2 text-sm leading-none font-medium select-none",
20
+ "group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
21
+ "peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
22
+ @attrs.delete(:class)
23
+ )
24
+ @attrs.merge(data_slot: "label", class: classes)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Menubar
6
+ class Menubar < 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
+ classes = cn(
19
+ "flex h-9 items-center gap-1 rounded-md border bg-background p-1 shadow-xs",
20
+ @attrs.delete(:class)
21
+ )
22
+ @attrs.merge(
23
+ data_slot: "menubar",
24
+ data_controller: "shadcn--menubar",
25
+ role: "menubar",
26
+ class: classes
27
+ )
28
+ end
29
+ end
30
+
31
+ class MenubarMenu < Base
32
+ def initialize(**attrs)
33
+ @attrs = attrs
34
+ end
35
+
36
+ def view_template(&block)
37
+ div(**build_attrs, &block)
38
+ end
39
+
40
+ private
41
+
42
+ def build_attrs
43
+ @attrs.merge(data_slot: "menubar-menu")
44
+ end
45
+ end
46
+
47
+ class MenubarTrigger < Base
48
+ def initialize(**attrs)
49
+ @attrs = attrs
50
+ end
51
+
52
+ def view_template(&block)
53
+ button(**build_attrs, &block)
54
+ end
55
+
56
+ private
57
+
58
+ def build_attrs
59
+ classes = cn(
60
+ "flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none cursor-default",
61
+ "focus:bg-accent focus:text-accent-foreground",
62
+ "data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
63
+ @attrs.delete(:class)
64
+ )
65
+ @attrs.merge(
66
+ data_slot: "menubar-trigger",
67
+ data_shadcn__menubar_target: "trigger",
68
+ data_action: "click->shadcn--menubar#toggleMenu mouseenter->shadcn--menubar#enterTrigger",
69
+ type: "button",
70
+ class: classes
71
+ )
72
+ end
73
+ end
74
+
75
+ class MenubarContent < Base
76
+ def initialize(**attrs)
77
+ @attrs = attrs
78
+ end
79
+
80
+ def view_template(&block)
81
+ div(**build_attrs, &block)
82
+ end
83
+
84
+ private
85
+
86
+ def build_attrs
87
+ classes = cn(
88
+ "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
89
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
90
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
91
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
92
+ "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
93
+ @attrs.delete(:class)
94
+ )
95
+ @attrs.merge(
96
+ data_slot: "menubar-content",
97
+ data_shadcn__menubar_target: "content",
98
+ role: "menu",
99
+ hidden: true,
100
+ class: classes
101
+ )
102
+ end
103
+ end
104
+
105
+ class MenubarItem < Base
106
+ def initialize(inset: false, variant: :default, **attrs)
107
+ @inset = inset
108
+ @variant = variant
109
+ @attrs = attrs
110
+ end
111
+
112
+ def view_template(&block)
113
+ div(**build_attrs, &block)
114
+ end
115
+
116
+ private
117
+
118
+ def build_attrs
119
+ classes = cn(
120
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
121
+ "focus:bg-accent focus:text-accent-foreground",
122
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
123
+ "data-[inset]:pl-8",
124
+ "data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10",
125
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
126
+ "[&_svg:not([class*='text-'])]:text-muted-foreground",
127
+ @attrs.delete(:class)
128
+ )
129
+ result = @attrs.merge(
130
+ data_slot: "menubar-item",
131
+ data_shadcn__menubar_target: "item",
132
+ data_action: "click->shadcn--menubar#selectItem",
133
+ data_variant: @variant,
134
+ role: "menuitem",
135
+ tabindex: "-1",
136
+ class: classes
137
+ )
138
+ result[:data_inset] = true if @inset
139
+ result
140
+ end
141
+ end
142
+
143
+ class MenubarCheckboxItem < Base
144
+ def initialize(checked: false, **attrs)
145
+ @checked = checked
146
+ @attrs = attrs
147
+ end
148
+
149
+ def view_template(&block)
150
+ div(**build_attrs) do
151
+ span(class: "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center") do
152
+ if @checked
153
+ svg(
154
+ xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16",
155
+ viewbox: "0 0 24 24", fill: "none", stroke: "currentColor",
156
+ stroke_width: "2", class: "size-4"
157
+ ) { |s| s.path(d: "M20 6 9 17l-5-5") }
158
+ end
159
+ end
160
+ yield if block_given?
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def build_attrs
167
+ classes = cn(
168
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm",
169
+ "outline-hidden select-none focus:bg-accent focus:text-accent-foreground",
170
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
171
+ @attrs.delete(:class)
172
+ )
173
+ @attrs.merge(
174
+ data_slot: "menubar-checkbox-item",
175
+ role: "menuitemcheckbox",
176
+ aria_checked: @checked,
177
+ class: classes
178
+ )
179
+ end
180
+ end
181
+
182
+ class MenubarRadioGroup < Base
183
+ def initialize(**attrs)
184
+ @attrs = attrs
185
+ end
186
+
187
+ def view_template(&block)
188
+ div(**build_attrs, &block)
189
+ end
190
+
191
+ private
192
+
193
+ def build_attrs
194
+ @attrs.merge(data_slot: "menubar-radio-group", role: "group")
195
+ end
196
+ end
197
+
198
+ class MenubarRadioItem < Base
199
+ def initialize(checked: false, **attrs)
200
+ @checked = checked
201
+ @attrs = attrs
202
+ end
203
+
204
+ def view_template(&block)
205
+ div(**build_attrs) do
206
+ span(class: "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center") do
207
+ if @checked
208
+ svg(
209
+ xmlns: "http://www.w3.org/2000/svg", width: "8", height: "8",
210
+ viewbox: "0 0 24 24", fill: "currentColor", class: "size-2"
211
+ ) { |s| s.circle(cx: "12", cy: "12", r: "10") }
212
+ end
213
+ end
214
+ yield if block_given?
215
+ end
216
+ end
217
+
218
+ private
219
+
220
+ def build_attrs
221
+ classes = cn(
222
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm",
223
+ "outline-hidden select-none focus:bg-accent focus:text-accent-foreground",
224
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
225
+ @attrs.delete(:class)
226
+ )
227
+ @attrs.merge(
228
+ data_slot: "menubar-radio-item",
229
+ role: "menuitemradio",
230
+ aria_checked: @checked,
231
+ class: classes
232
+ )
233
+ end
234
+ end
235
+
236
+ class MenubarLabel < Base
237
+ def initialize(inset: false, **attrs)
238
+ @inset = inset
239
+ @attrs = attrs
240
+ end
241
+
242
+ def view_template(&block)
243
+ div(**build_attrs, &block)
244
+ end
245
+
246
+ private
247
+
248
+ def build_attrs
249
+ classes = cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", @attrs.delete(:class))
250
+ result = @attrs.merge(data_slot: "menubar-label", class: classes)
251
+ result[:data_inset] = true if @inset
252
+ result
253
+ end
254
+ end
255
+
256
+ class MenubarSeparator < Base
257
+ def initialize(**attrs)
258
+ @attrs = attrs
259
+ end
260
+
261
+ def view_template
262
+ div(**build_attrs)
263
+ end
264
+
265
+ private
266
+
267
+ def build_attrs
268
+ classes = cn("-mx-1 my-1 h-px bg-muted", @attrs.delete(:class))
269
+ @attrs.merge(data_slot: "menubar-separator", role: "separator", class: classes)
270
+ end
271
+ end
272
+
273
+ class MenubarShortcut < Base
274
+ def initialize(**attrs)
275
+ @attrs = attrs
276
+ end
277
+
278
+ def view_template(&block)
279
+ span(**build_attrs, &block)
280
+ end
281
+
282
+ private
283
+
284
+ def build_attrs
285
+ classes = cn("ml-auto text-xs tracking-widest text-muted-foreground", @attrs.delete(:class))
286
+ @attrs.merge(data_slot: "menubar-shortcut", class: classes)
287
+ end
288
+ end
289
+
290
+ class MenubarSubTrigger < Base
291
+ def initialize(inset: false, **attrs)
292
+ @inset = inset
293
+ @attrs = attrs
294
+ end
295
+
296
+ def view_template(&block)
297
+ div(**build_attrs) do
298
+ yield if block_given?
299
+ svg(
300
+ xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16",
301
+ viewbox: "0 0 24 24", fill: "none", stroke: "currentColor",
302
+ stroke_width: "2", class: "ml-auto size-4"
303
+ ) { |s| s.path(d: "m9 18 6-6-6-6") }
304
+ end
305
+ end
306
+
307
+ private
308
+
309
+ def build_attrs
310
+ classes = cn(
311
+ "flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
312
+ "focus:bg-accent focus:text-accent-foreground",
313
+ "data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
314
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
315
+ @attrs.delete(:class)
316
+ )
317
+ result = @attrs.merge(data_slot: "menubar-sub-trigger", class: classes)
318
+ result[:data_inset] = true if @inset
319
+ result
320
+ end
321
+ end
322
+
323
+ class MenubarSubContent < Base
324
+ def initialize(**attrs)
325
+ @attrs = attrs
326
+ end
327
+
328
+ def view_template(&block)
329
+ div(**build_attrs, &block)
330
+ end
331
+
332
+ private
333
+
334
+ def build_attrs
335
+ classes = cn(
336
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
337
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
338
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
339
+ @attrs.delete(:class)
340
+ )
341
+ @attrs.merge(data_slot: "menubar-sub-content", class: classes)
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui NativeSelect
6
+ class NativeSelect < Base
7
+ def initialize(**attrs)
8
+ @attrs = attrs
9
+ end
10
+
11
+ def view_template(&block)
12
+ select(**build_attrs, &block)
13
+ end
14
+
15
+ private
16
+
17
+ def build_attrs
18
+ classes = cn(
19
+ "border-input flex h-9 w-full items-center rounded-md border bg-transparent px-3 py-1 text-base shadow-xs",
20
+ "transition-[color,box-shadow] outline-none",
21
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
22
+ "disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
23
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
24
+ "dark:bg-input/30",
25
+ @attrs.delete(:class)
26
+ )
27
+ @attrs.merge(data_slot: "native-select", class: classes)
28
+ end
29
+ end
30
+ end
31
+ end