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.
- checksums.yaml +7 -0
- data/README.md +195 -0
- data/app.css +20 -0
- data/css/shadcn-source.css +3 -0
- data/css/shadcn-tailwind.css +160 -0
- data/css/themes/mauve.css +62 -0
- data/css/themes/mist.css +62 -0
- data/css/themes/neutral.css +74 -0
- data/css/themes/olive.css +62 -0
- data/css/themes/stone.css +62 -0
- data/css/themes/taupe.css +62 -0
- data/css/themes/zinc.css +62 -0
- data/js/controllers/accordion_controller.js +135 -0
- data/js/controllers/checkbox_controller.js +52 -0
- data/js/controllers/collapsible_controller.js +85 -0
- data/js/controllers/combobox_controller.js +168 -0
- data/js/controllers/command_controller.js +171 -0
- data/js/controllers/context_menu_controller.js +132 -0
- data/js/controllers/dark_mode_controller.js +106 -0
- data/js/controllers/dialog_controller.js +205 -0
- data/js/controllers/drawer_controller.js +161 -0
- data/js/controllers/dropdown_menu_controller.js +189 -0
- data/js/controllers/hover_card_controller.js +85 -0
- data/js/controllers/index.js +89 -0
- data/js/controllers/menubar_controller.js +171 -0
- data/js/controllers/navigation_menu_controller.js +160 -0
- data/js/controllers/popover_controller.js +151 -0
- data/js/controllers/radio_group_controller.js +78 -0
- data/js/controllers/scroll_area_controller.js +117 -0
- data/js/controllers/select_controller.js +198 -0
- data/js/controllers/sheet_controller.js +130 -0
- data/js/controllers/slider_controller.js +142 -0
- data/js/controllers/switch_controller.js +40 -0
- data/js/controllers/tabs_controller.js +96 -0
- data/js/controllers/toast_controller.js +206 -0
- data/js/controllers/toggle_controller.js +30 -0
- data/js/controllers/toggle_group_controller.js +73 -0
- data/js/controllers/tooltip_controller.js +146 -0
- data/lib/generators/shadcn_phlex/component_generator.rb +79 -0
- data/lib/generators/shadcn_phlex/install_generator.rb +217 -0
- data/lib/shadcn/base.rb +27 -0
- data/lib/shadcn/engine.rb +24 -0
- data/lib/shadcn/kit.rb +1158 -0
- data/lib/shadcn/themes/accent_colors.rb +106 -0
- data/lib/shadcn/themes/base_colors.rb +313 -0
- data/lib/shadcn/ui/accordion.rb +135 -0
- data/lib/shadcn/ui/alert.rb +79 -0
- data/lib/shadcn/ui/alert_dialog.rb +220 -0
- data/lib/shadcn/ui/aspect_ratio.rb +35 -0
- data/lib/shadcn/ui/avatar.rb +134 -0
- data/lib/shadcn/ui/badge.rb +48 -0
- data/lib/shadcn/ui/breadcrumb.rb +180 -0
- data/lib/shadcn/ui/button.rb +63 -0
- data/lib/shadcn/ui/button_group.rb +58 -0
- data/lib/shadcn/ui/card.rb +133 -0
- data/lib/shadcn/ui/checkbox.rb +72 -0
- data/lib/shadcn/ui/collapsible.rb +76 -0
- data/lib/shadcn/ui/combobox.rb +229 -0
- data/lib/shadcn/ui/command.rb +256 -0
- data/lib/shadcn/ui/context_menu.rb +319 -0
- data/lib/shadcn/ui/dialog.rb +226 -0
- data/lib/shadcn/ui/direction.rb +23 -0
- data/lib/shadcn/ui/drawer.rb +217 -0
- data/lib/shadcn/ui/dropdown_menu.rb +384 -0
- data/lib/shadcn/ui/empty.rb +97 -0
- data/lib/shadcn/ui/field.rb +126 -0
- data/lib/shadcn/ui/hover_card.rb +75 -0
- data/lib/shadcn/ui/input.rb +36 -0
- data/lib/shadcn/ui/input_group.rb +32 -0
- data/lib/shadcn/ui/input_otp.rb +112 -0
- data/lib/shadcn/ui/item.rb +115 -0
- data/lib/shadcn/ui/kbd.rb +45 -0
- data/lib/shadcn/ui/label.rb +28 -0
- data/lib/shadcn/ui/menubar.rb +345 -0
- data/lib/shadcn/ui/native_select.rb +31 -0
- data/lib/shadcn/ui/navigation_menu.rb +238 -0
- data/lib/shadcn/ui/pagination.rb +224 -0
- data/lib/shadcn/ui/popover.rb +147 -0
- data/lib/shadcn/ui/progress.rb +40 -0
- data/lib/shadcn/ui/radio_group.rb +92 -0
- data/lib/shadcn/ui/resizable.rb +108 -0
- data/lib/shadcn/ui/scroll_area.rb +75 -0
- data/lib/shadcn/ui/select.rb +235 -0
- data/lib/shadcn/ui/separator.rb +36 -0
- data/lib/shadcn/ui/sheet.rb +231 -0
- data/lib/shadcn/ui/sidebar.rb +420 -0
- data/lib/shadcn/ui/skeleton.rb +23 -0
- data/lib/shadcn/ui/slider.rb +72 -0
- data/lib/shadcn/ui/sonner.rb +177 -0
- data/lib/shadcn/ui/spinner.rb +58 -0
- data/lib/shadcn/ui/switch.rb +75 -0
- data/lib/shadcn/ui/table.rb +154 -0
- data/lib/shadcn/ui/tabs.rb +154 -0
- data/lib/shadcn/ui/text_field.rb +146 -0
- data/lib/shadcn/ui/textarea.rb +32 -0
- data/lib/shadcn/ui/theme_toggle.rb +74 -0
- data/lib/shadcn/ui/toggle.rb +66 -0
- data/lib/shadcn/ui/toggle_group.rb +75 -0
- data/lib/shadcn/ui/tooltip.rb +78 -0
- data/lib/shadcn/ui/typography.rb +217 -0
- data/lib/shadcn/version.rb +5 -0
- data/lib/shadcn-phlex.rb +6 -0
- data/lib/shadcn.rb +80 -0
- data/package.json +14 -0
- data/skills/shadcn-phlex/SKILL.md +190 -0
- data/skills/shadcn-phlex/evals/evals.json +90 -0
- data/skills/shadcn-phlex/references/component-catalog.md +355 -0
- data/skills/shadcn-phlex/rules/composition.md +235 -0
- data/skills/shadcn-phlex/rules/forms.md +151 -0
- data/skills/shadcn-phlex/rules/helpers.md +54 -0
- data/skills/shadcn-phlex/rules/stimulus.md +61 -0
- data/skills/shadcn-phlex/rules/styling.md +177 -0
- metadata +209 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Component Catalog
|
|
2
|
+
|
|
3
|
+
Complete API reference for all shadcn-phlex components.
|
|
4
|
+
|
|
5
|
+
## Button
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
ui_button(
|
|
9
|
+
variant: :default, # :default, :destructive, :outline, :secondary, :ghost, :link
|
|
10
|
+
size: :default, # :default, :xs, :sm, :lg, :icon, :icon_xs, :icon_sm, :icon_lg
|
|
11
|
+
tag: :button, # :button, :a, or any HTML tag
|
|
12
|
+
class: nil, # additional Tailwind classes
|
|
13
|
+
**attrs # any HTML attributes (href:, disabled:, etc.)
|
|
14
|
+
) { content }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Badge
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
ui_badge(
|
|
21
|
+
variant: :default, # :default, :secondary, :destructive, :outline, :ghost, :link
|
|
22
|
+
**attrs
|
|
23
|
+
) { content }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Input
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
ui_input(
|
|
30
|
+
type: "text", # any HTML input type
|
|
31
|
+
**attrs # name:, placeholder:, value:, disabled:, etc.
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Textarea
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
ui_textarea(**attrs) { optional_default_content }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Label
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
ui_label(for: "input-id", **attrs) { "Label text" }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## TextField (compound)
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
ui_text_field(
|
|
51
|
+
label: "Email", # label text (nil to omit)
|
|
52
|
+
name: "user[email]", # form field name
|
|
53
|
+
type: "text", # input type
|
|
54
|
+
description: nil, # help text below input
|
|
55
|
+
error: nil, # error message (replaces description)
|
|
56
|
+
required: false, # adds asterisk, required attr
|
|
57
|
+
disabled: false,
|
|
58
|
+
id: nil, # auto-generated from name if nil
|
|
59
|
+
**input_attrs # placeholder:, value:, etc.
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## TextareaField (compound)
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
ui_textarea_field(
|
|
67
|
+
label: "Message",
|
|
68
|
+
name: "contact[message]",
|
|
69
|
+
description: nil,
|
|
70
|
+
error: nil,
|
|
71
|
+
required: false,
|
|
72
|
+
**textarea_attrs # rows:, cols:, placeholder:, etc.
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Checkbox
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
ui_checkbox(
|
|
80
|
+
checked: false,
|
|
81
|
+
name: nil, # renders hidden input when set
|
|
82
|
+
**attrs
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Switch
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
ui_switch(
|
|
90
|
+
checked: false,
|
|
91
|
+
size: :default, # :default, :sm
|
|
92
|
+
name: nil, # renders hidden input when set
|
|
93
|
+
**attrs
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Select
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
ui_select(name: nil) do # renders hidden input when name set
|
|
101
|
+
ui_select_trigger do
|
|
102
|
+
ui_select_value(placeholder: "Choose...")
|
|
103
|
+
end
|
|
104
|
+
ui_select_content do
|
|
105
|
+
ui_select_group do
|
|
106
|
+
ui_select_label { "Group" }
|
|
107
|
+
ui_select_item(value: "val") { "Display" }
|
|
108
|
+
end
|
|
109
|
+
ui_select_separator
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## RadioGroup
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
ui_radio_group(name: nil) do
|
|
118
|
+
ui_radio_group_item(value: "opt1", checked: true)
|
|
119
|
+
ui_radio_group_item(value: "opt2")
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Slider
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
ui_slider(value: 50, min: 0, max: 100, step: 1, name: nil)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Combobox
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
ui_combobox(name: nil) do
|
|
133
|
+
ui_combobox_trigger { ui_combobox_value(placeholder: "Search...") }
|
|
134
|
+
ui_combobox_content do
|
|
135
|
+
ui_combobox_input(placeholder: "Filter...")
|
|
136
|
+
ui_combobox_empty { "No results." }
|
|
137
|
+
ui_combobox_item(value: "val") { "Display" }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Card
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
ui_card do
|
|
146
|
+
ui_card_header do
|
|
147
|
+
ui_card_title { "Title" }
|
|
148
|
+
ui_card_description { "Description" }
|
|
149
|
+
ui_card_action { ui_button(size: :sm) { "Action" } }
|
|
150
|
+
end
|
|
151
|
+
ui_card_content { "Body" }
|
|
152
|
+
ui_card_footer { "Footer" }
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Dialog
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
ui_dialog do
|
|
160
|
+
ui_dialog_trigger { "Open" }
|
|
161
|
+
ui_dialog_content(show_close_button: true) do
|
|
162
|
+
ui_dialog_header do
|
|
163
|
+
ui_dialog_title { "Title" }
|
|
164
|
+
ui_dialog_description { "Description" }
|
|
165
|
+
end
|
|
166
|
+
# ... body ...
|
|
167
|
+
ui_dialog_footer do
|
|
168
|
+
ui_dialog_close { ui_button(variant: :outline) { "Cancel" } }
|
|
169
|
+
ui_button { "Save" }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## AlertDialog
|
|
176
|
+
|
|
177
|
+
Same structure as Dialog but overlay click does not close:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
ui_alert_dialog do
|
|
181
|
+
ui_alert_dialog_trigger { "Delete" }
|
|
182
|
+
ui_alert_dialog_content do
|
|
183
|
+
ui_alert_dialog_header do
|
|
184
|
+
ui_alert_dialog_title { "Are you sure?" }
|
|
185
|
+
ui_alert_dialog_description { "This action cannot be undone." }
|
|
186
|
+
end
|
|
187
|
+
ui_alert_dialog_footer do
|
|
188
|
+
ui_alert_dialog_cancel { "Cancel" }
|
|
189
|
+
ui_alert_dialog_action { "Delete" }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Sheet
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
ui_sheet do
|
|
199
|
+
ui_sheet_trigger { "Open" }
|
|
200
|
+
ui_sheet_content(side: :right) do # :top, :right, :bottom, :left
|
|
201
|
+
ui_sheet_header do
|
|
202
|
+
ui_sheet_title { "Edit Profile" }
|
|
203
|
+
ui_sheet_description { "Make changes here." }
|
|
204
|
+
end
|
|
205
|
+
# ... body ...
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Tabs
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
ui_tabs(value: "tab1", orientation: :horizontal) do
|
|
214
|
+
ui_tabs_list(variant: :default) do # :default, :line
|
|
215
|
+
ui_tabs_trigger(value: "tab1") { "Tab 1" }
|
|
216
|
+
ui_tabs_trigger(value: "tab2") { "Tab 2" }
|
|
217
|
+
end
|
|
218
|
+
ui_tabs_content(value: "tab1") { "Content 1" }
|
|
219
|
+
ui_tabs_content(value: "tab2") { "Content 2" }
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Accordion
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
ui_accordion(type: "single", collapsible: true) do # or type: "multiple"
|
|
227
|
+
ui_accordion_item(open: false) do
|
|
228
|
+
ui_accordion_trigger { "Section 1" }
|
|
229
|
+
ui_accordion_content { "Content 1" }
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Table
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
ui_table do
|
|
238
|
+
ui_table_header do
|
|
239
|
+
ui_table_row do
|
|
240
|
+
ui_table_head { "Name" }
|
|
241
|
+
ui_table_head { "Email" }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
ui_table_body do
|
|
245
|
+
@users.each do |user|
|
|
246
|
+
ui_table_row do
|
|
247
|
+
ui_table_cell { user.name }
|
|
248
|
+
ui_table_cell { user.email }
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Alert
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
ui_alert(variant: :default) do # :default, :destructive
|
|
259
|
+
# Optional SVG icon as first child
|
|
260
|
+
ui_alert_title { "Heads up!" }
|
|
261
|
+
ui_alert_description { "You can add components." }
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Avatar
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
ui_avatar do
|
|
269
|
+
ui_avatar_image(src: user.avatar_url, alt: user.name)
|
|
270
|
+
ui_avatar_fallback { user.initials }
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Separator
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
ui_separator(orientation: :horizontal) # :horizontal, :vertical
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Progress
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
ui_progress(value: 75)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Skeleton
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
ui_skeleton(class: "h-4 w-[250px]")
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Spinner
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
ui_spinner(size: :default) # :xs, :sm, :default, :lg
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## ThemeToggle
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
ui_theme_toggle # sun/moon toggle, persists to localStorage
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Tooltip
|
|
305
|
+
|
|
306
|
+
```ruby
|
|
307
|
+
ui_tooltip do
|
|
308
|
+
ui_tooltip_trigger { ui_button(variant: :outline) { "Hover me" } }
|
|
309
|
+
ui_tooltip_content { "Tooltip text" }
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## DropdownMenu
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
ui_dropdown_menu do
|
|
317
|
+
ui_dropdown_menu_trigger { ui_button { "Options" } }
|
|
318
|
+
ui_dropdown_menu_content do
|
|
319
|
+
ui_dropdown_menu_label { "Actions" }
|
|
320
|
+
ui_dropdown_menu_separator
|
|
321
|
+
ui_dropdown_menu_item { "Edit" }
|
|
322
|
+
ui_dropdown_menu_item(variant: :destructive) { "Delete" }
|
|
323
|
+
ui_dropdown_menu_separator
|
|
324
|
+
ui_dropdown_menu_shortcut { "⌘K" }
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Command (Cmd+K palette)
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
ui_command do
|
|
333
|
+
ui_command_dialog do
|
|
334
|
+
ui_command_input(placeholder: "Type a command...")
|
|
335
|
+
ui_command_list do
|
|
336
|
+
ui_command_empty { "No results." }
|
|
337
|
+
ui_command_group(heading: "Suggestions") do
|
|
338
|
+
ui_command_item(value: "calendar") { "Calendar" }
|
|
339
|
+
ui_command_item(value: "search") { "Search" }
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Theming
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
# Generate theme CSS
|
|
350
|
+
css = Shadcn::Themes.generate_css(
|
|
351
|
+
base_color: :zinc, # :neutral, :stone, :zinc, :mauve, :olive, :mist, :taupe
|
|
352
|
+
accent_color: :violet, # :blue, :red, :green, :orange, :violet, :amber, etc.
|
|
353
|
+
radius: "0.5rem"
|
|
354
|
+
)
|
|
355
|
+
```
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Component Composition
|
|
2
|
+
|
|
3
|
+
## Items always inside their parent
|
|
4
|
+
|
|
5
|
+
### Incorrect
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
ui_select(name: "role") do
|
|
9
|
+
ui_select_item(value: "admin") { "Admin" } # Items directly in Select
|
|
10
|
+
end
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Correct
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
ui_select(name: "role") do
|
|
17
|
+
ui_select_trigger { ui_select_value(placeholder: "Choose") }
|
|
18
|
+
ui_select_content do # Items inside Content
|
|
19
|
+
ui_select_item(value: "admin") { "Admin" }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Same rule applies to:
|
|
25
|
+
- `DropdownMenuItem` → inside `DropdownMenuContent`
|
|
26
|
+
- `CommandItem` → inside `CommandGroup`
|
|
27
|
+
- `ContextMenuItem` → inside `ContextMenuContent`
|
|
28
|
+
- `TabsTrigger` → inside `TabsList`
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Dialog, Sheet, and Drawer always need a Title
|
|
33
|
+
|
|
34
|
+
Required for accessibility. Use `class: "sr-only"` if visually hidden.
|
|
35
|
+
|
|
36
|
+
### Incorrect
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
ui_dialog do
|
|
40
|
+
ui_dialog_trigger { "Open" }
|
|
41
|
+
ui_dialog_content do
|
|
42
|
+
h2 { "Settings" } # Raw h2, not a DialogTitle
|
|
43
|
+
p { "Configure here." }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Correct
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
ui_dialog do
|
|
52
|
+
ui_dialog_trigger { "Open" }
|
|
53
|
+
ui_dialog_content do
|
|
54
|
+
ui_dialog_header do
|
|
55
|
+
ui_dialog_title { "Settings" }
|
|
56
|
+
ui_dialog_description { "Configure here." }
|
|
57
|
+
end
|
|
58
|
+
# body...
|
|
59
|
+
ui_dialog_footer do
|
|
60
|
+
ui_button(variant: :outline) { "Cancel" }
|
|
61
|
+
ui_button { "Save" }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For visually hidden title:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
ui_dialog_title(class: "sr-only") { "Settings" }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Use components instead of custom markup
|
|
76
|
+
|
|
77
|
+
### Incorrect
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
div(class: "animate-pulse rounded-md bg-gray-200 h-4 w-full") {}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Correct
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
ui_skeleton(class: "h-4 w-full")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Incorrect
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
span(class: "inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700") { "Active" }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Correct
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
ui_badge(variant: :secondary) { "Active" }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Incorrect
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
hr(class: "my-4 border-gray-200")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Correct
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
ui_separator
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Incorrect
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
div(class: "flex flex-col items-center justify-center p-8 text-center") do
|
|
117
|
+
h3 { "No results" }
|
|
118
|
+
p(class: "text-muted-foreground") { "Try adjusting your filters." }
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Correct
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
ui_empty do
|
|
126
|
+
ui_empty_title { "No results" }
|
|
127
|
+
ui_empty_description { "Try adjusting your filters." }
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Button has no loading prop
|
|
134
|
+
|
|
135
|
+
Compose with Spinner and disabled.
|
|
136
|
+
|
|
137
|
+
### Incorrect
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
ui_button(loading: true) { "Saving..." } # loading is not a prop
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Correct
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
ui_button(disabled: @saving) do
|
|
147
|
+
if @saving
|
|
148
|
+
ui_spinner(size: :sm)
|
|
149
|
+
plain " Saving..."
|
|
150
|
+
else
|
|
151
|
+
plain "Save"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Card content uses sub-components
|
|
159
|
+
|
|
160
|
+
### Incorrect
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
ui_card do
|
|
164
|
+
div(class: "p-6") do
|
|
165
|
+
h3(class: "font-semibold") { "Title" }
|
|
166
|
+
p(class: "text-sm text-gray-500") { "Description" }
|
|
167
|
+
end
|
|
168
|
+
div(class: "p-6") { "Content" }
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Correct
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
ui_card do
|
|
176
|
+
ui_card_header do
|
|
177
|
+
ui_card_title { "Title" }
|
|
178
|
+
ui_card_description { "Description" }
|
|
179
|
+
end
|
|
180
|
+
ui_card_content { "Content" }
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Tabs — trigger values must match content values
|
|
187
|
+
|
|
188
|
+
### Incorrect
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
ui_tabs do
|
|
192
|
+
ui_tabs_list do
|
|
193
|
+
ui_tabs_trigger(value: "one") { "Tab 1" }
|
|
194
|
+
end
|
|
195
|
+
div { "Content" } # raw div, no matching value
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Correct
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
ui_tabs(value: "one") do
|
|
203
|
+
ui_tabs_list do
|
|
204
|
+
ui_tabs_trigger(value: "one") { "Tab 1" }
|
|
205
|
+
ui_tabs_trigger(value: "two") { "Tab 2" }
|
|
206
|
+
end
|
|
207
|
+
ui_tabs_content(value: "one") { "Content 1" }
|
|
208
|
+
ui_tabs_content(value: "two") { "Content 2" }
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Choosing overlays
|
|
215
|
+
|
|
216
|
+
| Situation | Component |
|
|
217
|
+
|-----------|-----------|
|
|
218
|
+
| Needs user decision, blocks interaction | `AlertDialog` |
|
|
219
|
+
| Form or content, closeable by clicking overlay | `Dialog` |
|
|
220
|
+
| Side panel (settings, details) | `Sheet` |
|
|
221
|
+
| Mobile-friendly bottom panel | `Drawer` |
|
|
222
|
+
| Small interactive content on click | `Popover` |
|
|
223
|
+
| Rich preview on hover | `HoverCard` |
|
|
224
|
+
| Short text hint on hover | `Tooltip` |
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Avatar always needs a Fallback
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
ui_avatar do
|
|
232
|
+
ui_avatar_image(src: user.avatar_url, alt: user.name)
|
|
233
|
+
ui_avatar_fallback { user.initials }
|
|
234
|
+
end
|
|
235
|
+
```
|