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,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
+ ```