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,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui Command (cmdk-style command palette)
6
+ # Wired to shadcn--command Stimulus controller
7
+ class Command < 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 h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
21
+ @attrs.delete(:class)
22
+ )
23
+ @attrs.merge(
24
+ data_slot: "command",
25
+ data_controller: "shadcn--command",
26
+ class: classes
27
+ )
28
+ end
29
+ end
30
+
31
+ class CommandDialog < Base
32
+ def initialize(**attrs)
33
+ @attrs = attrs
34
+ end
35
+
36
+ def view_template(&block)
37
+ # Overlay
38
+ div(
39
+ data_slot: "command-overlay",
40
+ data_shadcn__command_target: "overlay",
41
+ data_action: "click->shadcn--command#clickOverlay",
42
+ class: "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
43
+ hidden: true
44
+ )
45
+
46
+ # Dialog
47
+ div(**build_attrs, &block)
48
+ end
49
+
50
+ private
51
+
52
+ def build_attrs
53
+ classes = cn(
54
+ "fixed top-[50%] left-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
55
+ "overflow-hidden rounded-lg border bg-popover p-0 shadow-lg",
56
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
57
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
58
+ @attrs.delete(:class)
59
+ )
60
+ @attrs.merge(
61
+ data_slot: "command-dialog",
62
+ data_shadcn__command_target: "dialog",
63
+ hidden: true,
64
+ class: classes
65
+ )
66
+ end
67
+ end
68
+
69
+ class CommandInput < Base
70
+ def initialize(placeholder: "Type a command or search...", **attrs)
71
+ @placeholder = placeholder
72
+ @attrs = attrs
73
+ end
74
+
75
+ def view_template
76
+ div(class: "flex items-center border-b px-3") do
77
+ svg(xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16",
78
+ viewbox: "0 0 24 24", fill: "none", stroke: "currentColor",
79
+ stroke_width: "2", class: "mr-2 size-4 shrink-0 opacity-50") do |s|
80
+ s.circle(cx: "11", cy: "11", r: "8")
81
+ s.path(d: "m21 21-4.3-4.3")
82
+ end
83
+ input(**build_attrs)
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def build_attrs
90
+ classes = cn(
91
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none",
92
+ "placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
93
+ @attrs.delete(:class)
94
+ )
95
+ @attrs.merge(
96
+ data_slot: "command-input",
97
+ data_shadcn__command_target: "input",
98
+ data_action: "input->shadcn--command#filter",
99
+ type: "text",
100
+ placeholder: @placeholder,
101
+ class: classes
102
+ )
103
+ end
104
+ end
105
+
106
+ class CommandList < Base
107
+ def initialize(**attrs)
108
+ @attrs = attrs
109
+ end
110
+
111
+ def view_template(&block)
112
+ div(**build_attrs, &block)
113
+ end
114
+
115
+ private
116
+
117
+ def build_attrs
118
+ classes = cn(
119
+ "max-h-[300px] overflow-y-auto overflow-x-hidden",
120
+ @attrs.delete(:class)
121
+ )
122
+ @attrs.merge(
123
+ data_slot: "command-list",
124
+ data_shadcn__command_target: "list",
125
+ class: classes
126
+ )
127
+ end
128
+ end
129
+
130
+ class CommandEmpty < Base
131
+ def initialize(**attrs)
132
+ @attrs = attrs
133
+ end
134
+
135
+ def view_template(&block)
136
+ div(**build_attrs) do
137
+ if block_given?
138
+ yield
139
+ else
140
+ plain "No results found."
141
+ end
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ def build_attrs
148
+ classes = cn("py-6 text-center text-sm", @attrs.delete(:class))
149
+ @attrs.merge(
150
+ data_slot: "command-empty",
151
+ data_shadcn__command_target: "empty",
152
+ hidden: true,
153
+ class: classes
154
+ )
155
+ end
156
+ end
157
+
158
+ class CommandGroup < Base
159
+ def initialize(heading: nil, **attrs)
160
+ @heading = heading
161
+ @attrs = attrs
162
+ end
163
+
164
+ def view_template(&block)
165
+ div(**build_attrs) do
166
+ if @heading
167
+ div(
168
+ data_slot: "command-group-heading",
169
+ class: "px-2 py-1.5 text-xs font-medium text-muted-foreground"
170
+ ) { @heading }
171
+ end
172
+ div(data_slot: "command-group-items", class: "p-1", &block)
173
+ end
174
+ end
175
+
176
+ private
177
+
178
+ def build_attrs
179
+ classes = cn(
180
+ "overflow-hidden text-foreground [&_[data-slot=command-group-heading]]:px-2 [&_[data-slot=command-group-heading]]:py-1.5 [&_[data-slot=command-group-heading]]:text-xs [&_[data-slot=command-group-heading]]:font-medium [&_[data-slot=command-group-heading]]:text-muted-foreground",
181
+ @attrs.delete(:class)
182
+ )
183
+ @attrs.merge(
184
+ data_slot: "command-group",
185
+ data_shadcn__command_target: "group",
186
+ class: classes
187
+ )
188
+ end
189
+ end
190
+
191
+ class CommandItem < Base
192
+ def initialize(value: nil, **attrs)
193
+ @value = value
194
+ @attrs = attrs
195
+ end
196
+
197
+ def view_template(&block)
198
+ div(**build_attrs, &block)
199
+ end
200
+
201
+ private
202
+
203
+ def build_attrs
204
+ classes = cn(
205
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
206
+ "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
207
+ "hover:bg-accent hover:text-accent-foreground",
208
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
209
+ @attrs.delete(:class)
210
+ )
211
+ @attrs.merge(
212
+ data_slot: "command-item",
213
+ data_shadcn__command_target: "item",
214
+ data_action: "click->shadcn--command#selectItem",
215
+ data_value: @value,
216
+ tabindex: "-1",
217
+ class: classes
218
+ )
219
+ end
220
+ end
221
+
222
+ class CommandSeparator < Base
223
+ def initialize(**attrs)
224
+ @attrs = attrs
225
+ end
226
+
227
+ def view_template
228
+ div(**build_attrs)
229
+ end
230
+
231
+ private
232
+
233
+ def build_attrs
234
+ classes = cn("-mx-1 h-px bg-border", @attrs.delete(:class))
235
+ @attrs.merge(data_slot: "command-separator", class: classes)
236
+ end
237
+ end
238
+
239
+ class CommandShortcut < Base
240
+ def initialize(**attrs)
241
+ @attrs = attrs
242
+ end
243
+
244
+ def view_template(&block)
245
+ span(**build_attrs, &block)
246
+ end
247
+
248
+ private
249
+
250
+ def build_attrs
251
+ classes = cn("ml-auto text-xs tracking-widest text-muted-foreground", @attrs.delete(:class))
252
+ @attrs.merge(data_slot: "command-shortcut", class: classes)
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module UI
5
+ # Port of shadcn/ui ContextMenu
6
+ class ContextMenu < 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: "context-menu",
20
+ data_controller: "shadcn--context-menu"
21
+ )
22
+ end
23
+ end
24
+
25
+ class ContextMenuTrigger < Base
26
+ def initialize(**attrs)
27
+ @attrs = attrs
28
+ end
29
+
30
+ def view_template(&block)
31
+ div(**build_attrs, &block)
32
+ end
33
+
34
+ private
35
+
36
+ def build_attrs
37
+ @attrs.merge(
38
+ data_slot: "context-menu-trigger",
39
+ data_shadcn__context_menu_target: "trigger"
40
+ )
41
+ end
42
+ end
43
+
44
+ class ContextMenuContent < Base
45
+ def initialize(**attrs)
46
+ @attrs = attrs
47
+ end
48
+
49
+ def view_template(&block)
50
+ div(**build_attrs, &block)
51
+ end
52
+
53
+ private
54
+
55
+ def build_attrs
56
+ classes = cn(
57
+ "z-50 max-h-[var(--radix-context-menu-content-available-height)] min-w-[8rem]",
58
+ "overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
59
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
60
+ "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
61
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
62
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
63
+ @attrs.delete(:class)
64
+ )
65
+ @attrs.merge(
66
+ data_slot: "context-menu-content",
67
+ data_shadcn__context_menu_target: "content",
68
+ role: "menu",
69
+ hidden: true,
70
+ class: classes
71
+ )
72
+ end
73
+ end
74
+
75
+ class ContextMenuItem < Base
76
+ def initialize(inset: false, variant: :default, **attrs)
77
+ @inset = inset
78
+ @variant = variant
79
+ @attrs = attrs
80
+ end
81
+
82
+ def view_template(&block)
83
+ div(**build_attrs, &block)
84
+ end
85
+
86
+ private
87
+
88
+ def build_attrs
89
+ classes = cn(
90
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
91
+ "focus:bg-accent focus:text-accent-foreground",
92
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
93
+ "data-[inset]:pl-8",
94
+ "data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive",
95
+ "dark:data-[variant=destructive]:focus:bg-destructive/20",
96
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
97
+ "[&_svg:not([class*='text-'])]:text-muted-foreground",
98
+ @attrs.delete(:class)
99
+ )
100
+ result = @attrs.merge(
101
+ data_slot: "context-menu-item",
102
+ data_shadcn__context_menu_target: "item",
103
+ data_action: "click->shadcn--context-menu#selectItem",
104
+ data_variant: @variant,
105
+ role: "menuitem",
106
+ tabindex: "-1",
107
+ class: classes
108
+ )
109
+ result[:data_inset] = true if @inset
110
+ result
111
+ end
112
+ end
113
+
114
+ class ContextMenuCheckboxItem < Base
115
+ def initialize(checked: false, **attrs)
116
+ @checked = checked
117
+ @attrs = attrs
118
+ end
119
+
120
+ def view_template(&block)
121
+ div(**build_attrs) do
122
+ span(class: "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center") do
123
+ if @checked
124
+ svg(
125
+ xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16",
126
+ viewbox: "0 0 24 24", fill: "none", stroke: "currentColor",
127
+ stroke_width: "2", class: "size-4"
128
+ ) { |s| s.path(d: "M20 6 9 17l-5-5") }
129
+ end
130
+ end
131
+ yield if block_given?
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def build_attrs
138
+ classes = cn(
139
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm",
140
+ "outline-hidden select-none focus:bg-accent focus:text-accent-foreground",
141
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
142
+ @attrs.delete(:class)
143
+ )
144
+ @attrs.merge(
145
+ data_slot: "context-menu-checkbox-item",
146
+ role: "menuitemcheckbox",
147
+ aria_checked: @checked,
148
+ class: classes
149
+ )
150
+ end
151
+ end
152
+
153
+ class ContextMenuRadioGroup < Base
154
+ def initialize(**attrs)
155
+ @attrs = attrs
156
+ end
157
+
158
+ def view_template(&block)
159
+ div(**build_attrs, &block)
160
+ end
161
+
162
+ private
163
+
164
+ def build_attrs
165
+ @attrs.merge(data_slot: "context-menu-radio-group", role: "group")
166
+ end
167
+ end
168
+
169
+ class ContextMenuRadioItem < Base
170
+ def initialize(checked: false, **attrs)
171
+ @checked = checked
172
+ @attrs = attrs
173
+ end
174
+
175
+ def view_template(&block)
176
+ div(**build_attrs) do
177
+ span(class: "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center") do
178
+ if @checked
179
+ svg(
180
+ xmlns: "http://www.w3.org/2000/svg", width: "8", height: "8",
181
+ viewbox: "0 0 24 24", fill: "currentColor", class: "size-2"
182
+ ) { |s| s.circle(cx: "12", cy: "12", r: "10") }
183
+ end
184
+ end
185
+ yield if block_given?
186
+ end
187
+ end
188
+
189
+ private
190
+
191
+ def build_attrs
192
+ classes = cn(
193
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm",
194
+ "outline-hidden select-none focus:bg-accent focus:text-accent-foreground",
195
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
196
+ @attrs.delete(:class)
197
+ )
198
+ @attrs.merge(
199
+ data_slot: "context-menu-radio-item",
200
+ role: "menuitemradio",
201
+ aria_checked: @checked,
202
+ class: classes
203
+ )
204
+ end
205
+ end
206
+
207
+ class ContextMenuLabel < Base
208
+ def initialize(inset: false, **attrs)
209
+ @inset = inset
210
+ @attrs = attrs
211
+ end
212
+
213
+ def view_template(&block)
214
+ div(**build_attrs, &block)
215
+ end
216
+
217
+ private
218
+
219
+ def build_attrs
220
+ classes = cn("px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8", @attrs.delete(:class))
221
+ result = @attrs.merge(data_slot: "context-menu-label", class: classes)
222
+ result[:data_inset] = true if @inset
223
+ result
224
+ end
225
+ end
226
+
227
+ class ContextMenuSeparator < Base
228
+ def initialize(**attrs)
229
+ @attrs = attrs
230
+ end
231
+
232
+ def view_template
233
+ div(**build_attrs)
234
+ end
235
+
236
+ private
237
+
238
+ def build_attrs
239
+ classes = cn("-mx-1 my-1 h-px bg-border", @attrs.delete(:class))
240
+ @attrs.merge(data_slot: "context-menu-separator", role: "separator", class: classes)
241
+ end
242
+ end
243
+
244
+ class ContextMenuShortcut < Base
245
+ def initialize(**attrs)
246
+ @attrs = attrs
247
+ end
248
+
249
+ def view_template(&block)
250
+ span(**build_attrs, &block)
251
+ end
252
+
253
+ private
254
+
255
+ def build_attrs
256
+ classes = cn("ml-auto text-xs tracking-widest text-muted-foreground", @attrs.delete(:class))
257
+ @attrs.merge(data_slot: "context-menu-shortcut", class: classes)
258
+ end
259
+ end
260
+
261
+ class ContextMenuSubTrigger < Base
262
+ def initialize(inset: false, **attrs)
263
+ @inset = inset
264
+ @attrs = attrs
265
+ end
266
+
267
+ def view_template(&block)
268
+ div(**build_attrs) do
269
+ yield if block_given?
270
+ svg(
271
+ xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16",
272
+ viewbox: "0 0 24 24", fill: "none", stroke: "currentColor",
273
+ stroke_width: "2", class: "ml-auto size-4"
274
+ ) { |s| s.path(d: "m9 18 6-6-6-6") }
275
+ end
276
+ end
277
+
278
+ private
279
+
280
+ def build_attrs
281
+ classes = cn(
282
+ "flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none",
283
+ "focus:bg-accent focus:text-accent-foreground",
284
+ "data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
285
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
286
+ "[&_svg:not([class*='text-'])]:text-muted-foreground",
287
+ @attrs.delete(:class)
288
+ )
289
+ result = @attrs.merge(data_slot: "context-menu-sub-trigger", class: classes)
290
+ result[:data_inset] = true if @inset
291
+ result
292
+ end
293
+ end
294
+
295
+ class ContextMenuSubContent < Base
296
+ def initialize(**attrs)
297
+ @attrs = attrs
298
+ end
299
+
300
+ def view_template(&block)
301
+ div(**build_attrs, &block)
302
+ end
303
+
304
+ private
305
+
306
+ def build_attrs
307
+ classes = cn(
308
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
309
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
310
+ "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
311
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
312
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
313
+ @attrs.delete(:class)
314
+ )
315
+ @attrs.merge(data_slot: "context-menu-sub-content", class: classes)
316
+ end
317
+ end
318
+ end
319
+ end