velvetkit 0.1.0.beta
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/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +197 -0
- data/lib/generators/velvetkit/add/add_generator.rb +111 -0
- data/lib/generators/velvetkit/ai/ai_generator.rb +78 -0
- data/lib/generators/velvetkit/helpers.rb +64 -0
- data/lib/generators/velvetkit/install/install_generator.rb +108 -0
- data/lib/velvetkit/namespace.rb +57 -0
- data/lib/velvetkit/reference.rb +108 -0
- data/lib/velvetkit/registry.rb +162 -0
- data/lib/velvetkit/version.rb +5 -0
- data/lib/velvetkit.rb +28 -0
- data/registry/app/assets/tailwind/velvetkit.css +342 -0
- data/registry/app/components/ui/accordion/content.html.erb +5 -0
- data/registry/app/components/ui/accordion/content.rb +34 -0
- data/registry/app/components/ui/accordion/item.html.erb +1 -0
- data/registry/app/components/ui/accordion/item.rb +40 -0
- data/registry/app/components/ui/accordion/trigger.html.erb +1 -0
- data/registry/app/components/ui/accordion/trigger.rb +37 -0
- data/registry/app/components/ui/accordion.html.erb +7 -0
- data/registry/app/components/ui/accordion.rb +34 -0
- data/registry/app/components/ui/alert/description.html.erb +1 -0
- data/registry/app/components/ui/alert/description.rb +12 -0
- data/registry/app/components/ui/alert/title.html.erb +1 -0
- data/registry/app/components/ui/alert/title.rb +12 -0
- data/registry/app/components/ui/alert.html.erb +16 -0
- data/registry/app/components/ui/alert.rb +55 -0
- data/registry/app/components/ui/avatar/fallback.html.erb +1 -0
- data/registry/app/components/ui/avatar/fallback.rb +12 -0
- data/registry/app/components/ui/avatar/image.rb +20 -0
- data/registry/app/components/ui/avatar.html.erb +1 -0
- data/registry/app/components/ui/avatar.rb +56 -0
- data/registry/app/components/ui/avatar_group.html.erb +1 -0
- data/registry/app/components/ui/avatar_group.rb +35 -0
- data/registry/app/components/ui/badge.html.erb +1 -0
- data/registry/app/components/ui/badge.rb +38 -0
- data/registry/app/components/ui/banner.html.erb +10 -0
- data/registry/app/components/ui/banner.rb +47 -0
- data/registry/app/components/ui/base.rb +53 -0
- data/registry/app/components/ui/breadcrumb/auto_ellipsis.html.erb +23 -0
- data/registry/app/components/ui/breadcrumb/auto_ellipsis.rb +12 -0
- data/registry/app/components/ui/breadcrumb/ellipsis.html.erb +18 -0
- data/registry/app/components/ui/breadcrumb/ellipsis.rb +8 -0
- data/registry/app/components/ui/breadcrumb/item.html.erb +1 -0
- data/registry/app/components/ui/breadcrumb/item.rb +12 -0
- data/registry/app/components/ui/breadcrumb/item_slot.html.erb +10 -0
- data/registry/app/components/ui/breadcrumb/item_slot.rb +47 -0
- data/registry/app/components/ui/breadcrumb/list.html.erb +1 -0
- data/registry/app/components/ui/breadcrumb/list.rb +12 -0
- data/registry/app/components/ui/breadcrumb/separator.html.erb +1 -0
- data/registry/app/components/ui/breadcrumb/separator.rb +12 -0
- data/registry/app/components/ui/breadcrumb.html.erb +30 -0
- data/registry/app/components/ui/breadcrumb.rb +83 -0
- data/registry/app/components/ui/button.html.erb +1 -0
- data/registry/app/components/ui/button.rb +81 -0
- data/registry/app/components/ui/button_to.html.erb +1 -0
- data/registry/app/components/ui/button_to.rb +33 -0
- data/registry/app/components/ui/card/content.html.erb +1 -0
- data/registry/app/components/ui/card/content.rb +12 -0
- data/registry/app/components/ui/card/description.html.erb +1 -0
- data/registry/app/components/ui/card/description.rb +12 -0
- data/registry/app/components/ui/card/footer.html.erb +1 -0
- data/registry/app/components/ui/card/footer.rb +12 -0
- data/registry/app/components/ui/card/header.html.erb +1 -0
- data/registry/app/components/ui/card/header.rb +15 -0
- data/registry/app/components/ui/card/title.html.erb +1 -0
- data/registry/app/components/ui/card/title.rb +12 -0
- data/registry/app/components/ui/card.html.erb +1 -0
- data/registry/app/components/ui/card.rb +18 -0
- data/registry/app/components/ui/checkbox.html.erb +18 -0
- data/registry/app/components/ui/checkbox.rb +133 -0
- data/registry/app/components/ui/data_grid.html.erb +2 -0
- data/registry/app/components/ui/data_grid.rb +84 -0
- data/registry/app/components/ui/date_picker.html.erb +70 -0
- data/registry/app/components/ui/date_picker.rb +111 -0
- data/registry/app/components/ui/dialog/close.html.erb +7 -0
- data/registry/app/components/ui/dialog/close.rb +53 -0
- data/registry/app/components/ui/dialog/content.html.erb +26 -0
- data/registry/app/components/ui/dialog/content.rb +102 -0
- data/registry/app/components/ui/dialog/description.html.erb +1 -0
- data/registry/app/components/ui/dialog/description.rb +16 -0
- data/registry/app/components/ui/dialog/footer.html.erb +1 -0
- data/registry/app/components/ui/dialog/footer.rb +14 -0
- data/registry/app/components/ui/dialog/header.html.erb +1 -0
- data/registry/app/components/ui/dialog/header.rb +16 -0
- data/registry/app/components/ui/dialog/title.html.erb +1 -0
- data/registry/app/components/ui/dialog/title.rb +16 -0
- data/registry/app/components/ui/dialog/trigger.html.erb +5 -0
- data/registry/app/components/ui/dialog/trigger.rb +75 -0
- data/registry/app/components/ui/dialog.html.erb +3 -0
- data/registry/app/components/ui/dialog.rb +41 -0
- data/registry/app/components/ui/divider.html.erb +9 -0
- data/registry/app/components/ui/divider.rb +41 -0
- data/registry/app/components/ui/icon.html.erb +1 -0
- data/registry/app/components/ui/icon.rb +41 -0
- data/registry/app/components/ui/image.html.erb +1 -0
- data/registry/app/components/ui/image.rb +74 -0
- data/registry/app/components/ui/input.html.erb +9 -0
- data/registry/app/components/ui/input.rb +127 -0
- data/registry/app/components/ui/input_group/addon.html.erb +6 -0
- data/registry/app/components/ui/input_group/addon.rb +51 -0
- data/registry/app/components/ui/input_group/field.rb +26 -0
- data/registry/app/components/ui/input_group.html.erb +5 -0
- data/registry/app/components/ui/input_group.rb +55 -0
- data/registry/app/components/ui/label.html.erb +2 -0
- data/registry/app/components/ui/label.rb +48 -0
- data/registry/app/components/ui/link.html.erb +1 -0
- data/registry/app/components/ui/link.rb +64 -0
- data/registry/app/components/ui/menu/content.html.erb +5 -0
- data/registry/app/components/ui/menu/content.rb +106 -0
- data/registry/app/components/ui/menu/group.html.erb +1 -0
- data/registry/app/components/ui/menu/group.rb +19 -0
- data/registry/app/components/ui/menu/item.html.erb +1 -0
- data/registry/app/components/ui/menu/item.rb +68 -0
- data/registry/app/components/ui/menu/label.html.erb +1 -0
- data/registry/app/components/ui/menu/label.rb +33 -0
- data/registry/app/components/ui/menu/separator.html.erb +1 -0
- data/registry/app/components/ui/menu/separator.rb +20 -0
- data/registry/app/components/ui/menu/trigger.html.erb +1 -0
- data/registry/app/components/ui/menu/trigger.rb +29 -0
- data/registry/app/components/ui/menu.html.erb +3 -0
- data/registry/app/components/ui/menu.rb +23 -0
- data/registry/app/components/ui/native_select.html.erb +16 -0
- data/registry/app/components/ui/native_select.rb +109 -0
- data/registry/app/components/ui/pagination.html.erb +34 -0
- data/registry/app/components/ui/pagination.rb +62 -0
- data/registry/app/components/ui/password_input.html.erb +20 -0
- data/registry/app/components/ui/password_input.rb +94 -0
- data/registry/app/components/ui/popover/content.html.erb +12 -0
- data/registry/app/components/ui/popover/content.rb +83 -0
- data/registry/app/components/ui/popover/trigger.html.erb +1 -0
- data/registry/app/components/ui/popover/trigger.rb +29 -0
- data/registry/app/components/ui/popover.html.erb +3 -0
- data/registry/app/components/ui/popover.rb +24 -0
- data/registry/app/components/ui/radio_group/item.html.erb +18 -0
- data/registry/app/components/ui/radio_group/item.rb +97 -0
- data/registry/app/components/ui/radio_group.html.erb +1 -0
- data/registry/app/components/ui/radio_group.rb +35 -0
- data/registry/app/components/ui/select.html.erb +60 -0
- data/registry/app/components/ui/select.rb +181 -0
- data/registry/app/components/ui/sheet/close.rb +4 -0
- data/registry/app/components/ui/sheet/content.html.erb +24 -0
- data/registry/app/components/ui/sheet/content.rb +97 -0
- data/registry/app/components/ui/sheet/trigger.rb +4 -0
- data/registry/app/components/ui/sheet.html.erb +3 -0
- data/registry/app/components/ui/sheet.rb +37 -0
- data/registry/app/components/ui/sidebar/content.html.erb +1 -0
- data/registry/app/components/ui/sidebar/content.rb +18 -0
- data/registry/app/components/ui/sidebar/footer.html.erb +3 -0
- data/registry/app/components/ui/sidebar/footer.rb +16 -0
- data/registry/app/components/ui/sidebar/group.html.erb +1 -0
- data/registry/app/components/ui/sidebar/group.rb +19 -0
- data/registry/app/components/ui/sidebar/group_label.html.erb +1 -0
- data/registry/app/components/ui/sidebar/group_label.rb +18 -0
- data/registry/app/components/ui/sidebar/header.html.erb +1 -0
- data/registry/app/components/ui/sidebar/header.rb +16 -0
- data/registry/app/components/ui/sidebar/inset.html.erb +1 -0
- data/registry/app/components/ui/sidebar/inset.rb +17 -0
- data/registry/app/components/ui/sidebar/menu.html.erb +1 -0
- data/registry/app/components/ui/sidebar/menu.rb +18 -0
- data/registry/app/components/ui/sidebar/menu_button.html.erb +1 -0
- data/registry/app/components/ui/sidebar/menu_button.rb +30 -0
- data/registry/app/components/ui/sidebar/menu_item.html.erb +1 -0
- data/registry/app/components/ui/sidebar/menu_item.rb +23 -0
- data/registry/app/components/ui/sidebar/trigger.html.erb +1 -0
- data/registry/app/components/ui/sidebar/trigger.rb +24 -0
- data/registry/app/components/ui/sidebar/wrapper.html.erb +1 -0
- data/registry/app/components/ui/sidebar/wrapper.rb +40 -0
- data/registry/app/components/ui/sidebar.html.erb +11 -0
- data/registry/app/components/ui/sidebar.rb +29 -0
- data/registry/app/components/ui/spinner.html.erb +1 -0
- data/registry/app/components/ui/spinner.rb +20 -0
- data/registry/app/components/ui/switch.html.erb +12 -0
- data/registry/app/components/ui/switch.rb +109 -0
- data/registry/app/components/ui/table/cell.html.erb +1 -0
- data/registry/app/components/ui/table/cell.rb +16 -0
- data/registry/app/components/ui/table/column.html.erb +1 -0
- data/registry/app/components/ui/table/column.rb +18 -0
- data/registry/app/components/ui/table/head.html.erb +1 -0
- data/registry/app/components/ui/table/head.rb +13 -0
- data/registry/app/components/ui/table/row.html.erb +1 -0
- data/registry/app/components/ui/table/row.rb +26 -0
- data/registry/app/components/ui/table.html.erb +4 -0
- data/registry/app/components/ui/table.rb +45 -0
- data/registry/app/components/ui/tabs/content.html.erb +1 -0
- data/registry/app/components/ui/tabs/content.rb +43 -0
- data/registry/app/components/ui/tabs/list.html.erb +1 -0
- data/registry/app/components/ui/tabs/list.rb +28 -0
- data/registry/app/components/ui/tabs/trigger.html.erb +1 -0
- data/registry/app/components/ui/tabs/trigger.rb +91 -0
- data/registry/app/components/ui/tabs.html.erb +3 -0
- data/registry/app/components/ui/tabs.rb +27 -0
- data/registry/app/components/ui/text.html.erb +1 -0
- data/registry/app/components/ui/text.rb +48 -0
- data/registry/app/components/ui/theme_toggle.html.erb +9 -0
- data/registry/app/components/ui/theme_toggle.rb +63 -0
- data/registry/app/components/ui/tooltip/content.html.erb +1 -0
- data/registry/app/components/ui/tooltip/content.rb +65 -0
- data/registry/app/components/ui/tooltip/trigger.html.erb +1 -0
- data/registry/app/components/ui/tooltip/trigger.rb +33 -0
- data/registry/app/components/ui/tooltip.html.erb +3 -0
- data/registry/app/components/ui/tooltip.rb +24 -0
- data/registry/app/components/ui/typography/blockquote.html.erb +1 -0
- data/registry/app/components/ui/typography/blockquote.rb +15 -0
- data/registry/app/components/ui/typography/code.html.erb +1 -0
- data/registry/app/components/ui/typography/code.rb +25 -0
- data/registry/app/components/ui/typography/h1.rb +7 -0
- data/registry/app/components/ui/typography/h2.rb +7 -0
- data/registry/app/components/ui/typography/h3.rb +7 -0
- data/registry/app/components/ui/typography/h4.rb +7 -0
- data/registry/app/components/ui/typography/large.rb +7 -0
- data/registry/app/components/ui/typography/lead.rb +7 -0
- data/registry/app/components/ui/typography/muted.rb +7 -0
- data/registry/app/components/ui/typography/small.rb +7 -0
- data/registry/app/javascript/ag_grid.ts +26 -0
- data/registry/app/javascript/cally.ts +8 -0
- data/registry/app/javascript/controllers/ui/_internal/use_dismiss.ts +88 -0
- data/registry/app/javascript/controllers/ui/_internal/use_floating.ts +112 -0
- data/registry/app/javascript/controllers/ui/accordion_controller.ts +100 -0
- data/registry/app/javascript/controllers/ui/alert_controller.ts +7 -0
- data/registry/app/javascript/controllers/ui/banner_controller.ts +7 -0
- data/registry/app/javascript/controllers/ui/data_grid_controller.ts +198 -0
- data/registry/app/javascript/controllers/ui/date_picker_controller.ts +141 -0
- data/registry/app/javascript/controllers/ui/dialog_controller.ts +70 -0
- data/registry/app/javascript/controllers/ui/menu_controller.ts +201 -0
- data/registry/app/javascript/controllers/ui/password_visibility_controller.ts +43 -0
- data/registry/app/javascript/controllers/ui/popover_controller.ts +108 -0
- data/registry/app/javascript/controllers/ui/select_controller.ts +274 -0
- data/registry/app/javascript/controllers/ui/sidebar_controller.ts +64 -0
- data/registry/app/javascript/controllers/ui/tabs_controller.ts +74 -0
- data/registry/app/javascript/controllers/ui/theme_controller.ts +101 -0
- data/registry/app/javascript/controllers/ui/tooltip_controller.ts +102 -0
- data/registry/app/javascript/velvetkit_icons.ts +58 -0
- data/registry/app/views/layouts/_theme_script.html.erb +19 -0
- data/registry/components.yml +102 -0
- data/registry/config/locales/velvetkit.en.yml +56 -0
- data/registry/docs/agents.md +62 -0
- data/registry/test/components/previews/ui/accordion_preview/faq_style.html.erb +20 -0
- data/registry/test/components/previews/ui/accordion_preview/multiple.html.erb +27 -0
- data/registry/test/components/previews/ui/accordion_preview/single.html.erb +22 -0
- data/registry/test/components/previews/ui/accordion_preview.rb +31 -0
- data/registry/test/components/previews/ui/alert_preview/title_only.html.erb +12 -0
- data/registry/test/components/previews/ui/alert_preview/variants.html.erb +22 -0
- data/registry/test/components/previews/ui/alert_preview/with_icon.html.erb +12 -0
- data/registry/test/components/previews/ui/alert_preview/with_link.html.erb +18 -0
- data/registry/test/components/previews/ui/alert_preview.rb +34 -0
- data/registry/test/components/previews/ui/avatar_group_preview/default.html.erb +11 -0
- data/registry/test/components/previews/ui/avatar_group_preview/overflow.html.erb +23 -0
- data/registry/test/components/previews/ui/avatar_group_preview/small.html.erb +12 -0
- data/registry/test/components/previews/ui/avatar_group_preview.rb +23 -0
- data/registry/test/components/previews/ui/avatar_preview/colors.html.erb +8 -0
- data/registry/test/components/previews/ui/avatar_preview/shapes.html.erb +10 -0
- data/registry/test/components/previews/ui/avatar_preview/sizes.html.erb +8 -0
- data/registry/test/components/previews/ui/avatar_preview/with_image.html.erb +10 -0
- data/registry/test/components/previews/ui/avatar_preview.rb +24 -0
- data/registry/test/components/previews/ui/badge_preview/sizes.html.erb +5 -0
- data/registry/test/components/previews/ui/badge_preview/variants.html.erb +8 -0
- data/registry/test/components/previews/ui/badge_preview/with_icon.html.erb +14 -0
- data/registry/test/components/previews/ui/badge_preview.rb +20 -0
- data/registry/test/components/previews/ui/banner_preview/introduction.html.erb +9 -0
- data/registry/test/components/previews/ui/banner_preview/promotion.html.erb +9 -0
- data/registry/test/components/previews/ui/banner_preview/with_illustration.html.erb +12 -0
- data/registry/test/components/previews/ui/banner_preview.rb +25 -0
- data/registry/test/components/previews/ui/breadcrumb_preview/collapsed.html.erb +50 -0
- data/registry/test/components/previews/ui/breadcrumb_preview/default.html.erb +8 -0
- data/registry/test/components/previews/ui/breadcrumb_preview/long_path.html.erb +8 -0
- data/registry/test/components/previews/ui/breadcrumb_preview/medium.html.erb +7 -0
- data/registry/test/components/previews/ui/breadcrumb_preview/with_avatar.html.erb +8 -0
- data/registry/test/components/previews/ui/breadcrumb_preview.rb +66 -0
- data/registry/test/components/previews/ui/button_preview/disabled.html.erb +39 -0
- data/registry/test/components/previews/ui/button_preview/icon_only.html.erb +17 -0
- data/registry/test/components/previews/ui/button_preview/loading.html.erb +12 -0
- data/registry/test/components/previews/ui/button_preview/sizes.html.erb +6 -0
- data/registry/test/components/previews/ui/button_preview/variants.html.erb +8 -0
- data/registry/test/components/previews/ui/button_preview/with_both_icons.html.erb +12 -0
- data/registry/test/components/previews/ui/button_preview/with_leading_icon.html.erb +22 -0
- data/registry/test/components/previews/ui/button_preview/with_trailing_icon.html.erb +14 -0
- data/registry/test/components/previews/ui/button_preview.rb +65 -0
- data/registry/test/components/previews/ui/button_to_preview/states.html.erb +8 -0
- data/registry/test/components/previews/ui/button_to_preview/variants.html.erb +5 -0
- data/registry/test/components/previews/ui/button_to_preview/with_icon.html.erb +11 -0
- data/registry/test/components/previews/ui/button_to_preview.rb +21 -0
- data/registry/test/components/previews/ui/card_preview/complete.html.erb +31 -0
- data/registry/test/components/previews/ui/card_preview/default.html.erb +7 -0
- data/registry/test/components/previews/ui/card_preview/settings_form.html.erb +25 -0
- data/registry/test/components/previews/ui/card_preview/with_footer.html.erb +13 -0
- data/registry/test/components/previews/ui/card_preview/with_header.html.erb +11 -0
- data/registry/test/components/previews/ui/card_preview.rb +44 -0
- data/registry/test/components/previews/ui/checkbox_preview/in_form.html.erb +9 -0
- data/registry/test/components/previews/ui/checkbox_preview/states.html.erb +22 -0
- data/registry/test/components/previews/ui/checkbox_preview/with_label.html.erb +5 -0
- data/registry/test/components/previews/ui/checkbox_preview.rb +27 -0
- data/registry/test/components/previews/ui/data_grid_preview.rb +18 -0
- data/registry/test/components/previews/ui/date_picker_preview/default.html.erb +3 -0
- data/registry/test/components/previews/ui/date_picker_preview/states.html.erb +7 -0
- data/registry/test/components/previews/ui/date_picker_preview.rb +13 -0
- data/registry/test/components/previews/ui/dialog_preview/confirmation.html.erb +11 -0
- data/registry/test/components/previews/ui/dialog_preview/custom_trigger.html.erb +16 -0
- data/registry/test/components/previews/ui/dialog_preview/default.html.erb +15 -0
- data/registry/test/components/previews/ui/dialog_preview/long_content.html.erb +16 -0
- data/registry/test/components/previews/ui/dialog_preview/playground.html.erb +12 -0
- data/registry/test/components/previews/ui/dialog_preview/scrollable_body.html.erb +29 -0
- data/registry/test/components/previews/ui/dialog_preview/sizes.html.erb +46 -0
- data/registry/test/components/previews/ui/dialog_preview/without_close_button.html.erb +11 -0
- data/registry/test/components/previews/ui/dialog_preview/without_title.html.erb +13 -0
- data/registry/test/components/previews/ui/dialog_preview.rb +38 -0
- data/registry/test/components/previews/ui/divider_preview/horizontal.html.erb +5 -0
- data/registry/test/components/previews/ui/divider_preview/vertical.html.erb +9 -0
- data/registry/test/components/previews/ui/divider_preview/with_label.html.erb +5 -0
- data/registry/test/components/previews/ui/divider_preview.rb +19 -0
- data/registry/test/components/previews/ui/icon_preview/common.html.erb +8 -0
- data/registry/test/components/previews/ui/icon_preview/sizes.html.erb +7 -0
- data/registry/test/components/previews/ui/icon_preview.rb +20 -0
- data/registry/test/components/previews/ui/image_preview/radii.html.erb +8 -0
- data/registry/test/components/previews/ui/image_preview/sizing.html.erb +9 -0
- data/registry/test/components/previews/ui/image_preview.rb +16 -0
- data/registry/test/components/previews/ui/input_group_preview/addons.html.erb +29 -0
- data/registry/test/components/previews/ui/input_group_preview/playground.html.erb +12 -0
- data/registry/test/components/previews/ui/input_group_preview.rb +17 -0
- data/registry/test/components/previews/ui/input_preview/sizes.html.erb +5 -0
- data/registry/test/components/previews/ui/input_preview/states.html.erb +7 -0
- data/registry/test/components/previews/ui/input_preview/with_labels.html.erb +10 -0
- data/registry/test/components/previews/ui/input_preview.rb +33 -0
- data/registry/test/components/previews/ui/label_preview/sizes.html.erb +6 -0
- data/registry/test/components/previews/ui/label_preview/states.html.erb +6 -0
- data/registry/test/components/previews/ui/label_preview/weights.html.erb +5 -0
- data/registry/test/components/previews/ui/label_preview.rb +22 -0
- data/registry/test/components/previews/ui/link_preview/sizes.html.erb +5 -0
- data/registry/test/components/previews/ui/link_preview/variants.html.erb +8 -0
- data/registry/test/components/previews/ui/link_preview/with_icons.html.erb +11 -0
- data/registry/test/components/previews/ui/link_preview.rb +20 -0
- data/registry/test/components/previews/ui/menu_preview/positions.html.erb +122 -0
- data/registry/test/components/previews/ui/menu_preview/variants.html.erb +218 -0
- data/registry/test/components/previews/ui/menu_preview.rb +26 -0
- data/registry/test/components/previews/ui/native_select_preview/sizes.html.erb +6 -0
- data/registry/test/components/previews/ui/native_select_preview/states.html.erb +6 -0
- data/registry/test/components/previews/ui/native_select_preview.rb +23 -0
- data/registry/test/components/previews/ui/pagination_preview/states.html.erb +15 -0
- data/registry/test/components/previews/ui/pagination_preview.rb +15 -0
- data/registry/test/components/previews/ui/password_input_preview/states.html.erb +19 -0
- data/registry/test/components/previews/ui/password_input_preview.rb +21 -0
- data/registry/test/components/previews/ui/popover_preview/default.html.erb +7 -0
- data/registry/test/components/previews/ui/popover_preview/positions.html.erb +30 -0
- data/registry/test/components/previews/ui/popover_preview/with_close_button.html.erb +8 -0
- data/registry/test/components/previews/ui/popover_preview/with_rich_content.html.erb +17 -0
- data/registry/test/components/previews/ui/popover_preview/without_title.html.erb +9 -0
- data/registry/test/components/previews/ui/popover_preview.rb +32 -0
- data/registry/test/components/previews/ui/radio_group_preview/default.html.erb +10 -0
- data/registry/test/components/previews/ui/radio_group_preview/disabled.html.erb +17 -0
- data/registry/test/components/previews/ui/radio_group_preview/with_required.html.erb +10 -0
- data/registry/test/components/previews/ui/radio_group_preview.rb +25 -0
- data/registry/test/components/previews/ui/select_preview/default.html.erb +4 -0
- data/registry/test/components/previews/ui/select_preview/long_list.html.erb +5 -0
- data/registry/test/components/previews/ui/select_preview/multiple.html.erb +6 -0
- data/registry/test/components/previews/ui/select_preview/sizes.html.erb +5 -0
- data/registry/test/components/previews/ui/select_preview/states.html.erb +6 -0
- data/registry/test/components/previews/ui/select_preview.rb +45 -0
- data/registry/test/components/previews/ui/sheet_preview/bottom.html.erb +12 -0
- data/registry/test/components/previews/ui/sheet_preview/left.html.erb +10 -0
- data/registry/test/components/previews/ui/sheet_preview/playground.html.erb +15 -0
- data/registry/test/components/previews/ui/sheet_preview/right.html.erb +11 -0
- data/registry/test/components/previews/ui/sheet_preview/top.html.erb +8 -0
- data/registry/test/components/previews/ui/sheet_preview.rb +22 -0
- data/registry/test/components/previews/ui/sidebar_preview/_brand.html.erb +14 -0
- data/registry/test/components/previews/ui/sidebar_preview/_shell.html.erb +60 -0
- data/registry/test/components/previews/ui/sidebar_preview/collapsible.html.erb +55 -0
- data/registry/test/components/previews/ui/sidebar_preview/default.html.erb +17 -0
- data/registry/test/components/previews/ui/sidebar_preview/with_dropdown_header.html.erb +33 -0
- data/registry/test/components/previews/ui/sidebar_preview/with_groups.html.erb +22 -0
- data/registry/test/components/previews/ui/sidebar_preview.rb +15 -0
- data/registry/test/components/previews/ui/spinner_preview/in_buttons.html.erb +7 -0
- data/registry/test/components/previews/ui/spinner_preview/sizes.html.erb +7 -0
- data/registry/test/components/previews/ui/spinner_preview.rb +19 -0
- data/registry/test/components/previews/ui/switch_preview/default.html.erb +4 -0
- data/registry/test/components/previews/ui/switch_preview/sizes.html.erb +5 -0
- data/registry/test/components/previews/ui/switch_preview/states.html.erb +7 -0
- data/registry/test/components/previews/ui/switch_preview.rb +26 -0
- data/registry/test/components/previews/ui/table_preview/customers.html.erb +15 -0
- data/registry/test/components/previews/ui/table_preview/empty.html.erb +8 -0
- data/registry/test/components/previews/ui/table_preview/with_caption.html.erb +14 -0
- data/registry/test/components/previews/ui/table_preview.rb +12 -0
- data/registry/test/components/previews/ui/tabs_preview/default.html.erb +39 -0
- data/registry/test/components/previews/ui/tabs_preview/pill_variant.html.erb +18 -0
- data/registry/test/components/previews/ui/tabs_preview/settings_form.html.erb +42 -0
- data/registry/test/components/previews/ui/tabs_preview/with_icons.html.erb +34 -0
- data/registry/test/components/previews/ui/tabs_preview.rb +33 -0
- data/registry/test/components/previews/ui/text_preview/sizes.html.erb +10 -0
- data/registry/test/components/previews/ui/text_preview/tones.html.erb +5 -0
- data/registry/test/components/previews/ui/text_preview/weights.html.erb +6 -0
- data/registry/test/components/previews/ui/text_preview.rb +26 -0
- data/registry/test/components/previews/ui/theme_toggle_preview/in_a_header.html.erb +14 -0
- data/registry/test/components/previews/ui/theme_toggle_preview/palette.html.erb +47 -0
- data/registry/test/components/previews/ui/theme_toggle_preview.rb +15 -0
- data/registry/test/components/previews/ui/tooltip_preview/default.html.erb +8 -0
- data/registry/test/components/previews/ui/tooltip_preview/positions.html.erb +34 -0
- data/registry/test/components/previews/ui/tooltip_preview/with_button.html.erb +8 -0
- data/registry/test/components/previews/ui/tooltip_preview/with_icon.html.erb +10 -0
- data/registry/test/components/previews/ui/tooltip_preview.rb +29 -0
- data/registry/test/components/previews/ui/typography_preview/article.html.erb +11 -0
- data/registry/test/components/previews/ui/typography_preview/code_and_quotes.html.erb +6 -0
- data/registry/test/components/previews/ui/typography_preview/headings.html.erb +6 -0
- data/registry/test/components/previews/ui/typography_preview/text_styles.html.erb +7 -0
- data/registry/test/components/previews/ui/typography_preview.rb +23 -0
- data/registry/test/components/ui/accordion/content_test.rb +64 -0
- data/registry/test/components/ui/accordion/item_test.rb +61 -0
- data/registry/test/components/ui/accordion/trigger_test.rb +80 -0
- data/registry/test/components/ui/accordion_test.rb +96 -0
- data/registry/test/components/ui/alert/description_test.rb +37 -0
- data/registry/test/components/ui/alert/title_test.rb +38 -0
- data/registry/test/components/ui/alert_test.rb +184 -0
- data/registry/test/components/ui/avatar_group_test.rb +79 -0
- data/registry/test/components/ui/avatar_test.rb +129 -0
- data/registry/test/components/ui/badge_test.rb +112 -0
- data/registry/test/components/ui/banner_test.rb +116 -0
- data/registry/test/components/ui/base_test.rb +98 -0
- data/registry/test/components/ui/breadcrumb/ellipsis_test.rb +51 -0
- data/registry/test/components/ui/breadcrumb/item_test.rb +39 -0
- data/registry/test/components/ui/breadcrumb/list_test.rb +34 -0
- data/registry/test/components/ui/breadcrumb/separator_test.rb +36 -0
- data/registry/test/components/ui/breadcrumb_test.rb +276 -0
- data/registry/test/components/ui/button_test.rb +276 -0
- data/registry/test/components/ui/button_to_test.rb +74 -0
- data/registry/test/components/ui/card/content_test.rb +41 -0
- data/registry/test/components/ui/card/description_test.rb +32 -0
- data/registry/test/components/ui/card/footer_test.rb +34 -0
- data/registry/test/components/ui/card/header_test.rb +57 -0
- data/registry/test/components/ui/card/title_test.rb +34 -0
- data/registry/test/components/ui/card_test.rb +67 -0
- data/registry/test/components/ui/checkbox_test.rb +236 -0
- data/registry/test/components/ui/data_grid_test.rb +88 -0
- data/registry/test/components/ui/date_picker_test.rb +168 -0
- data/registry/test/components/ui/dialog/content_test.rb +156 -0
- data/registry/test/components/ui/dialog/trigger_test.rb +74 -0
- data/registry/test/components/ui/dialog_test.rb +54 -0
- data/registry/test/components/ui/divider_test.rb +69 -0
- data/registry/test/components/ui/icon_test.rb +82 -0
- data/registry/test/components/ui/image_test.rb +74 -0
- data/registry/test/components/ui/input_group_test.rb +143 -0
- data/registry/test/components/ui/input_test.rb +251 -0
- data/registry/test/components/ui/label_test.rb +96 -0
- data/registry/test/components/ui/link_test.rb +95 -0
- data/registry/test/components/ui/menu/content_test.rb +233 -0
- data/registry/test/components/ui/menu/group_test.rb +24 -0
- data/registry/test/components/ui/menu/item_test.rb +76 -0
- data/registry/test/components/ui/menu/label_test.rb +43 -0
- data/registry/test/components/ui/menu/separator_test.rb +32 -0
- data/registry/test/components/ui/menu/trigger_test.rb +109 -0
- data/registry/test/components/ui/menu_test.rb +152 -0
- data/registry/test/components/ui/native_select_test.rb +118 -0
- data/registry/test/components/ui/pagination_test.rb +92 -0
- data/registry/test/components/ui/password_input_test.rb +98 -0
- data/registry/test/components/ui/popover/content_test.rb +226 -0
- data/registry/test/components/ui/popover/trigger_test.rb +83 -0
- data/registry/test/components/ui/popover_test.rb +101 -0
- data/registry/test/components/ui/radio_group/item_test.rb +14 -0
- data/registry/test/components/ui/radio_group_test.rb +139 -0
- data/registry/test/components/ui/select_test.rb +181 -0
- data/registry/test/components/ui/sheet/content_test.rb +135 -0
- data/registry/test/components/ui/sheet/trigger_test.rb +23 -0
- data/registry/test/components/ui/sheet_test.rb +43 -0
- data/registry/test/components/ui/sidebar/menu_button_test.rb +43 -0
- data/registry/test/components/ui/sidebar/trigger_test.rb +17 -0
- data/registry/test/components/ui/sidebar/wrapper_test.rb +15 -0
- data/registry/test/components/ui/sidebar_test.rb +43 -0
- data/registry/test/components/ui/spinner_test.rb +63 -0
- data/registry/test/components/ui/switch_test.rb +206 -0
- data/registry/test/components/ui/table_test.rb +106 -0
- data/registry/test/components/ui/tabs/content_test.rb +93 -0
- data/registry/test/components/ui/tabs/list_test.rb +65 -0
- data/registry/test/components/ui/tabs/trigger_test.rb +159 -0
- data/registry/test/components/ui/tabs_test.rb +88 -0
- data/registry/test/components/ui/text_test.rb +84 -0
- data/registry/test/components/ui/theme_toggle_test.rb +88 -0
- data/registry/test/components/ui/tooltip/content_test.rb +135 -0
- data/registry/test/components/ui/tooltip/trigger_test.rb +52 -0
- data/registry/test/components/ui/tooltip_test.rb +97 -0
- data/registry/test/components/ui/typography/code_blockquote_test.rb +57 -0
- data/registry/test/components/ui/typography/heading_test.rb +71 -0
- data/registry/test/components/ui/typography/text_variants_test.rb +71 -0
- metadata +540 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
export default class extends Controller<HTMLElement> {
|
|
4
|
+
static targets = ['trigger', 'panel'];
|
|
5
|
+
static values = { active: String };
|
|
6
|
+
|
|
7
|
+
declare readonly triggerTargets: HTMLButtonElement[];
|
|
8
|
+
declare readonly panelTargets: HTMLElement[];
|
|
9
|
+
declare activeValue: string;
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
// If no active value is set, activate the first non-disabled trigger
|
|
13
|
+
const value = this.activeValue || this.firstEnabledTriggerValue();
|
|
14
|
+
this.sync(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private firstEnabledTriggerValue(): string {
|
|
18
|
+
const enabled = this.triggerTargets.filter((t) => !t.disabled);
|
|
19
|
+
return enabled[0]?.dataset.value || '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
activate(event: Event) {
|
|
23
|
+
const btn = event.currentTarget as HTMLButtonElement;
|
|
24
|
+
if (btn.disabled) return;
|
|
25
|
+
const value = btn.dataset.value!;
|
|
26
|
+
this.activeValue = value;
|
|
27
|
+
this.sync(value);
|
|
28
|
+
btn.focus();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
keydown(event: KeyboardEvent) {
|
|
32
|
+
const enabled = this.triggerTargets.filter((t) => !t.disabled);
|
|
33
|
+
if (enabled.length === 0) return;
|
|
34
|
+
const currentIndex = enabled.indexOf(event.currentTarget as HTMLButtonElement);
|
|
35
|
+
let next = -1;
|
|
36
|
+
switch (event.key) {
|
|
37
|
+
case 'ArrowRight':
|
|
38
|
+
next = (currentIndex + 1) % enabled.length;
|
|
39
|
+
break;
|
|
40
|
+
case 'ArrowLeft':
|
|
41
|
+
next = (currentIndex - 1 + enabled.length) % enabled.length;
|
|
42
|
+
break;
|
|
43
|
+
case 'Home':
|
|
44
|
+
next = 0;
|
|
45
|
+
break;
|
|
46
|
+
case 'End':
|
|
47
|
+
next = enabled.length - 1;
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
const target = enabled[next];
|
|
54
|
+
if (!target) return;
|
|
55
|
+
this.activeValue = target.dataset.value!;
|
|
56
|
+
this.sync(this.activeValue);
|
|
57
|
+
target.focus();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private sync(value: string) {
|
|
61
|
+
this.triggerTargets.forEach((t) => {
|
|
62
|
+
const active = t.dataset.value === value;
|
|
63
|
+
t.dataset.state = active ? 'active' : 'inactive';
|
|
64
|
+
t.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
65
|
+
t.tabIndex = active ? 0 : -1;
|
|
66
|
+
});
|
|
67
|
+
this.panelTargets.forEach((p) => {
|
|
68
|
+
const active = p.dataset.value === value;
|
|
69
|
+
p.dataset.state = active ? 'active' : 'inactive';
|
|
70
|
+
if (active) p.removeAttribute('hidden');
|
|
71
|
+
else p.setAttribute('hidden', '');
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="ui--theme"
|
|
4
|
+
//
|
|
5
|
+
// Applies the `.dark` class to <html>, which is what `@custom-variant dark`
|
|
6
|
+
// in application.css keys off. The stored preference is one of three values;
|
|
7
|
+
// 'system' defers to the OS and keeps following it while selected. With
|
|
8
|
+
// nothing stored, the page is light.
|
|
9
|
+
//
|
|
10
|
+
// The same resolution logic runs in an inline <head> script (see
|
|
11
|
+
// layouts/_theme_script.html.erb) so the class is set before first paint. This
|
|
12
|
+
// controller only handles changes made after load.
|
|
13
|
+
type ThemePreference = 'system' | 'light' | 'dark';
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = 'ui-theme';
|
|
16
|
+
|
|
17
|
+
export default class extends Controller<HTMLElement> {
|
|
18
|
+
static targets = ['option'];
|
|
19
|
+
|
|
20
|
+
declare readonly hasOptionTarget: boolean;
|
|
21
|
+
declare readonly optionTargets: HTMLElement[];
|
|
22
|
+
|
|
23
|
+
private media?: MediaQueryList;
|
|
24
|
+
private onSystemChange?: () => void;
|
|
25
|
+
|
|
26
|
+
connect() {
|
|
27
|
+
this.media = window.matchMedia('(prefers-color-scheme: dark)');
|
|
28
|
+
// An OS-level switch should move the page live, but only while the user
|
|
29
|
+
// has not pinned an explicit preference.
|
|
30
|
+
this.onSystemChange = () => {
|
|
31
|
+
if (this.preference() === 'system') this.apply();
|
|
32
|
+
};
|
|
33
|
+
this.media.addEventListener('change', this.onSystemChange);
|
|
34
|
+
this.apply();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
disconnect() {
|
|
38
|
+
if (this.media && this.onSystemChange) {
|
|
39
|
+
this.media.removeEventListener('change', this.onSystemChange);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// data-action="ui--theme#select" data-theme="dark"
|
|
44
|
+
select(event: Event) {
|
|
45
|
+
const chosen = (event.currentTarget as HTMLElement)?.dataset.theme as
|
|
46
|
+
| ThemePreference
|
|
47
|
+
| undefined;
|
|
48
|
+
if (!chosen) return;
|
|
49
|
+
|
|
50
|
+
localStorage.setItem(STORAGE_KEY, chosen);
|
|
51
|
+
this.apply();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Cycles light -> dark -> system, for a single-button toggle.
|
|
55
|
+
cycle() {
|
|
56
|
+
const order: ThemePreference[] = ['light', 'dark', 'system'];
|
|
57
|
+
const next = order[(order.indexOf(this.preference()) + 1) % order.length];
|
|
58
|
+
localStorage.setItem(STORAGE_KEY, next);
|
|
59
|
+
this.apply();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private preference(): ThemePreference {
|
|
63
|
+
const stored = this.readStored();
|
|
64
|
+
return stored === 'light' || stored === 'dark' || stored === 'system'
|
|
65
|
+
? stored
|
|
66
|
+
: 'light';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Private windows and blocked site data make localStorage throw on access,
|
|
70
|
+
// so every read is guarded; the page still renders in the light theme.
|
|
71
|
+
private readStored(): string | null {
|
|
72
|
+
try {
|
|
73
|
+
return localStorage.getItem(STORAGE_KEY);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private resolved(): 'light' | 'dark' {
|
|
80
|
+
const preference = this.preference();
|
|
81
|
+
if (preference !== 'system') return preference;
|
|
82
|
+
return this.media?.matches ? 'dark' : 'light';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private apply() {
|
|
86
|
+
const dark = this.resolved() === 'dark';
|
|
87
|
+
document.documentElement.classList.toggle('dark', dark);
|
|
88
|
+
document.documentElement.dataset.theme = this.preference();
|
|
89
|
+
this.syncOptions();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private syncOptions() {
|
|
93
|
+
if (!this.hasOptionTarget) return;
|
|
94
|
+
|
|
95
|
+
const preference = this.preference();
|
|
96
|
+
this.optionTargets.forEach((option) => {
|
|
97
|
+
const selected = option.dataset.theme === preference;
|
|
98
|
+
option.setAttribute('aria-checked', selected ? 'true' : 'false');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
import { useFloating, type Placement } from './_internal/use_floating.js';
|
|
3
|
+
|
|
4
|
+
export default class extends Controller<HTMLElement> {
|
|
5
|
+
static targets = ['trigger', 'content', 'arrow'];
|
|
6
|
+
|
|
7
|
+
declare readonly triggerTarget: HTMLElement;
|
|
8
|
+
declare readonly contentTarget: HTMLElement;
|
|
9
|
+
declare readonly arrowTarget: HTMLElement;
|
|
10
|
+
|
|
11
|
+
private floatingCleanup: (() => void) | null = null;
|
|
12
|
+
private showTimeout: number | null = null;
|
|
13
|
+
private hideTimeout: number | null = null;
|
|
14
|
+
private isVisible = false;
|
|
15
|
+
|
|
16
|
+
// Configuration
|
|
17
|
+
private readonly openDelay = 150;
|
|
18
|
+
private readonly closeDelay = 100;
|
|
19
|
+
|
|
20
|
+
connect() {
|
|
21
|
+
// Initialize with content hidden
|
|
22
|
+
this.hideContent();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
disconnect() {
|
|
26
|
+
this.clearTimeouts();
|
|
27
|
+
this.cleanupFloating();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
show() {
|
|
31
|
+
this.clearTimeouts();
|
|
32
|
+
this.showTimeout = window.setTimeout(() => {
|
|
33
|
+
this.showContent();
|
|
34
|
+
}, this.openDelay);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
hide() {
|
|
38
|
+
this.clearTimeouts();
|
|
39
|
+
this.hideTimeout = window.setTimeout(() => {
|
|
40
|
+
this.hideContent();
|
|
41
|
+
}, this.closeDelay);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private showContent() {
|
|
45
|
+
if (this.isVisible) return;
|
|
46
|
+
|
|
47
|
+
const placement = this.getPlacement();
|
|
48
|
+
// Offset = visible gap (8) + arrow protrusion (~6: 4px staticSide + ~2px
|
|
49
|
+
// diamond corner from rotate-45). Without this margin the arrow tip hugs
|
|
50
|
+
// the trigger edge and reads as "overlapped".
|
|
51
|
+
const floating = useFloating(this.triggerTarget, this.contentTarget, {
|
|
52
|
+
placement,
|
|
53
|
+
offset: 14,
|
|
54
|
+
flip: true,
|
|
55
|
+
shift: true,
|
|
56
|
+
arrow: this.arrowTarget,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.floatingCleanup = floating.destroy;
|
|
60
|
+
this.contentTarget.removeAttribute('hidden');
|
|
61
|
+
this.contentTarget.dataset.state = 'open';
|
|
62
|
+
this.isVisible = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private hideContent() {
|
|
66
|
+
if (!this.isVisible) return;
|
|
67
|
+
|
|
68
|
+
this.cleanupFloating();
|
|
69
|
+
this.contentTarget.setAttribute('hidden', '');
|
|
70
|
+
this.contentTarget.dataset.state = 'closed';
|
|
71
|
+
this.isVisible = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private cleanupFloating() {
|
|
75
|
+
if (this.floatingCleanup) {
|
|
76
|
+
this.floatingCleanup();
|
|
77
|
+
this.floatingCleanup = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private clearTimeouts() {
|
|
82
|
+
if (this.showTimeout) {
|
|
83
|
+
clearTimeout(this.showTimeout);
|
|
84
|
+
this.showTimeout = null;
|
|
85
|
+
}
|
|
86
|
+
if (this.hideTimeout) {
|
|
87
|
+
clearTimeout(this.hideTimeout);
|
|
88
|
+
this.hideTimeout = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private getPlacement(): Placement {
|
|
93
|
+
const side = this.contentTarget.dataset.side || 'top';
|
|
94
|
+
const align = this.contentTarget.dataset.align || 'center';
|
|
95
|
+
|
|
96
|
+
if (align === 'center') {
|
|
97
|
+
return side as Placement;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return `${side}-${align}` as Placement;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Lucide icons for VelvetKit components.
|
|
2
|
+
//
|
|
3
|
+
// The Icon component renders `<i data-lucide="name">`; createIcons swaps each
|
|
4
|
+
// one for its SVG. Only the icons listed here are bundled, so an icon your
|
|
5
|
+
// own views use has to be added in BOTH places below — one only in the import
|
|
6
|
+
// list renders as an empty element with a console warning and no error.
|
|
7
|
+
import {
|
|
8
|
+
createIcons,
|
|
9
|
+
Calendar,
|
|
10
|
+
Check,
|
|
11
|
+
ChevronDown,
|
|
12
|
+
ChevronLeft,
|
|
13
|
+
ChevronRight,
|
|
14
|
+
ChevronsUpDown,
|
|
15
|
+
CircleAlert,
|
|
16
|
+
Ellipsis,
|
|
17
|
+
Eye,
|
|
18
|
+
EyeOff,
|
|
19
|
+
Info,
|
|
20
|
+
Loader,
|
|
21
|
+
Mail,
|
|
22
|
+
Monitor,
|
|
23
|
+
Moon,
|
|
24
|
+
PanelLeft,
|
|
25
|
+
Sun,
|
|
26
|
+
X,
|
|
27
|
+
} from 'lucide';
|
|
28
|
+
|
|
29
|
+
const icons = {
|
|
30
|
+
Calendar,
|
|
31
|
+
Check,
|
|
32
|
+
ChevronDown,
|
|
33
|
+
ChevronLeft,
|
|
34
|
+
ChevronRight,
|
|
35
|
+
ChevronsUpDown,
|
|
36
|
+
CircleAlert,
|
|
37
|
+
Ellipsis,
|
|
38
|
+
Eye,
|
|
39
|
+
EyeOff,
|
|
40
|
+
Info,
|
|
41
|
+
Loader,
|
|
42
|
+
Mail,
|
|
43
|
+
Monitor,
|
|
44
|
+
Moon,
|
|
45
|
+
PanelLeft,
|
|
46
|
+
Sun,
|
|
47
|
+
X,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Turbo replaces markup without a full page load, so icons are drawn again
|
|
51
|
+
// after every kind of render, not just the first.
|
|
52
|
+
[
|
|
53
|
+
'turbo:load',
|
|
54
|
+
'turbo:frame-load',
|
|
55
|
+
'turbo:render',
|
|
56
|
+
'turbo:submit-end',
|
|
57
|
+
'turbo:render-stream',
|
|
58
|
+
].forEach((event) => document.addEventListener(event, () => createIcons({ icons })));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%# Resolves the stored theme and sets `.dark` on <html> BEFORE first paint.
|
|
2
|
+
The JS bundle is loaded as a deferred module, so it runs after paint —
|
|
3
|
+
without this blocking script every page flashes light before going dark.
|
|
4
|
+
Mirrors the resolution logic in controllers/ui/theme_controller.ts.
|
|
5
|
+
Render it inside <head> with render "layouts/theme_script". %>
|
|
6
|
+
<%= javascript_tag nonce: true do %>
|
|
7
|
+
(function () {
|
|
8
|
+
try {
|
|
9
|
+
var stored = null;
|
|
10
|
+
try { stored = localStorage.getItem('ui-theme'); } catch (e) {}
|
|
11
|
+
var pref = stored === 'dark' || stored === 'system' ? stored : 'light';
|
|
12
|
+
var dark = pref === 'dark' ||
|
|
13
|
+
(pref === 'system' &&
|
|
14
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
15
|
+
document.documentElement.classList.toggle('dark', dark);
|
|
16
|
+
document.documentElement.dataset.theme = pref;
|
|
17
|
+
} catch (e) {}
|
|
18
|
+
})();
|
|
19
|
+
<% end %>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# The VelvetKit catalog.
|
|
2
|
+
#
|
|
3
|
+
# Only what the source files cannot say lives here. Which files make up a
|
|
4
|
+
# component, which other components it renders, and which Stimulus controllers
|
|
5
|
+
# and npm packages it needs are all read from the files themselves by
|
|
6
|
+
# Velvetkit::Registry, so they cannot drift out of date.
|
|
7
|
+
|
|
8
|
+
# Versions of the npm packages the components import.
|
|
9
|
+
packages:
|
|
10
|
+
"@floating-ui/dom": "^1.7.4"
|
|
11
|
+
"@hotwired/stimulus": "^3.2.2"
|
|
12
|
+
ag-grid-community: "^36.1.0"
|
|
13
|
+
cally: "^0.9.2"
|
|
14
|
+
lucide: "^0.545.0"
|
|
15
|
+
tw-animate-css: "^1.4.0"
|
|
16
|
+
|
|
17
|
+
# Versions of the gems a component needs beyond view_component and tailwind_merge.
|
|
18
|
+
gems:
|
|
19
|
+
pagy: "~> 43.0"
|
|
20
|
+
|
|
21
|
+
components:
|
|
22
|
+
accordion:
|
|
23
|
+
description: Sections that expand and collapse, one at a time or several.
|
|
24
|
+
alert:
|
|
25
|
+
description: A highlighted message, optionally dismissible.
|
|
26
|
+
avatar:
|
|
27
|
+
description: A person's picture, with initials when there is none.
|
|
28
|
+
avatar_group:
|
|
29
|
+
description: Overlapping avatars with a "+N" count for the rest.
|
|
30
|
+
badge:
|
|
31
|
+
description: A small status label.
|
|
32
|
+
banner:
|
|
33
|
+
description: A dismissible strip across the top of a page.
|
|
34
|
+
base:
|
|
35
|
+
description: The base class every component inherits — variants and class merging.
|
|
36
|
+
breadcrumb:
|
|
37
|
+
description: The path to the current page, collapsing long trails.
|
|
38
|
+
button:
|
|
39
|
+
description: Buttons in every variant and size, with icons and a loading state.
|
|
40
|
+
button_to:
|
|
41
|
+
description: A button that submits a one-off form, like Rails' button_to.
|
|
42
|
+
card:
|
|
43
|
+
description: A bordered container with header, content and footer.
|
|
44
|
+
checkbox:
|
|
45
|
+
description: A checkbox with label, standalone or bound to a form builder.
|
|
46
|
+
data_grid:
|
|
47
|
+
description: A sortable, filterable, server-paged table on AG Grid.
|
|
48
|
+
date_picker:
|
|
49
|
+
description: A typed date field with a calendar, on Cally.
|
|
50
|
+
dialog:
|
|
51
|
+
description: A modal dialog on the native <dialog> element.
|
|
52
|
+
divider:
|
|
53
|
+
description: A horizontal or vertical rule, optionally labelled.
|
|
54
|
+
icon:
|
|
55
|
+
description: A Lucide icon.
|
|
56
|
+
image:
|
|
57
|
+
description: An image with consistent sizing and corner radius.
|
|
58
|
+
input:
|
|
59
|
+
description: A text field with label, hint and error, standalone or with a form builder.
|
|
60
|
+
input_group:
|
|
61
|
+
description: A text field with addons before or after it.
|
|
62
|
+
label:
|
|
63
|
+
description: A form label with a required marker.
|
|
64
|
+
link:
|
|
65
|
+
description: A link styled as text or as a button.
|
|
66
|
+
menu:
|
|
67
|
+
description: A dropdown menu with keyboard navigation.
|
|
68
|
+
pagination:
|
|
69
|
+
description: Page links for a long list, from a Pagy object.
|
|
70
|
+
gems: [pagy]
|
|
71
|
+
password_input:
|
|
72
|
+
description: A password field with a show/hide toggle.
|
|
73
|
+
popover:
|
|
74
|
+
description: Floating content anchored to a trigger.
|
|
75
|
+
radio_group:
|
|
76
|
+
description: A set of radio buttons.
|
|
77
|
+
native_select:
|
|
78
|
+
description: The browser's own select, styled closed; best for phones' system pickers.
|
|
79
|
+
select:
|
|
80
|
+
description: A styled dropdown with keyboard navigation and type-ahead, posting like a native select.
|
|
81
|
+
sheet:
|
|
82
|
+
description: A panel that slides in from an edge of the screen.
|
|
83
|
+
sidebar:
|
|
84
|
+
description: A collapsible application sidebar with groups and menus.
|
|
85
|
+
spinner:
|
|
86
|
+
description: A loading indicator.
|
|
87
|
+
switch:
|
|
88
|
+
description: An on/off toggle.
|
|
89
|
+
table:
|
|
90
|
+
description: A styled table with header, rows and caption.
|
|
91
|
+
tabs:
|
|
92
|
+
description: Tabbed panels with keyboard navigation.
|
|
93
|
+
text:
|
|
94
|
+
description: Body text in set sizes, weights and tones.
|
|
95
|
+
theme_toggle:
|
|
96
|
+
description: A light/dark/system theme switcher.
|
|
97
|
+
files:
|
|
98
|
+
- app/views/layouts/_theme_script.html.erb
|
|
99
|
+
tooltip:
|
|
100
|
+
description: A hint shown on hover and focus.
|
|
101
|
+
typography:
|
|
102
|
+
description: Headings, lead, muted, code and blockquote text styles.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Strings rendered by VelvetKit components. Translate by copying this file
|
|
2
|
+
# to velvetkit.<locale>.yml and changing the values, never the keys.
|
|
3
|
+
en:
|
|
4
|
+
date:
|
|
5
|
+
formats:
|
|
6
|
+
# How DatePicker writes and reads a date: its placeholder, parse order
|
|
7
|
+
# and separator all come from this one string. Change it per locale.
|
|
8
|
+
typed: "%d/%m/%Y"
|
|
9
|
+
ui:
|
|
10
|
+
date_picker:
|
|
11
|
+
open: 'Open the calendar'
|
|
12
|
+
# `format` is a reserved I18n interpolation key, hence `pattern`.
|
|
13
|
+
hint: 'Type the date as %{pattern}, or use the calendar'
|
|
14
|
+
previous: 'Previous month'
|
|
15
|
+
next: 'Next month'
|
|
16
|
+
data_grid:
|
|
17
|
+
empty: 'Nothing to show'
|
|
18
|
+
breadcrumb:
|
|
19
|
+
label: 'Breadcrumb'
|
|
20
|
+
ellipsis_label: 'More breadcrumb pages'
|
|
21
|
+
collapsed_pages_label: 'Collapsed breadcrumb pages'
|
|
22
|
+
alert:
|
|
23
|
+
close_label: 'Close alert'
|
|
24
|
+
button:
|
|
25
|
+
loading: 'Loading, please wait'
|
|
26
|
+
spinner:
|
|
27
|
+
label: 'Loading'
|
|
28
|
+
dialog:
|
|
29
|
+
close_label: 'Close'
|
|
30
|
+
password_input:
|
|
31
|
+
show: 'Show password'
|
|
32
|
+
hide: 'Hide password'
|
|
33
|
+
sidebar:
|
|
34
|
+
toggle: 'Toggle sidebar'
|
|
35
|
+
pagination:
|
|
36
|
+
label: 'Pages'
|
|
37
|
+
summary: 'Showing %{from}–%{to} of %{count}'
|
|
38
|
+
previous: 'Go to previous page'
|
|
39
|
+
previous_label: 'Previous'
|
|
40
|
+
next: 'Go to next page'
|
|
41
|
+
next_label: 'Next'
|
|
42
|
+
more: 'More pages'
|
|
43
|
+
select:
|
|
44
|
+
placeholder: 'Choose…'
|
|
45
|
+
search: 'Search'
|
|
46
|
+
search_label: 'Search the options'
|
|
47
|
+
no_matches: 'No matches'
|
|
48
|
+
# Shown on a multiple select once more than two options are ticked.
|
|
49
|
+
selected: '%{count} selected'
|
|
50
|
+
theme_toggle:
|
|
51
|
+
label: 'Change theme'
|
|
52
|
+
heading: 'Theme'
|
|
53
|
+
options:
|
|
54
|
+
light: 'Light'
|
|
55
|
+
dark: 'Dark'
|
|
56
|
+
system: 'System'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# VelvetKit components in this app
|
|
2
|
+
|
|
3
|
+
This app builds its UI from VelvetKit components: ViewComponents styled with
|
|
4
|
+
Tailwind CSS 4 and wired up with Stimulus. They were copied into the app by
|
|
5
|
+
the `velvetkit` gem and are now the app's own code, under `UI::`.
|
|
6
|
+
|
|
7
|
+
Use them for every new screen. Reach for raw HTML and Tailwind only when no
|
|
8
|
+
component fits, and never build a second version of a component that exists.
|
|
9
|
+
|
|
10
|
+
## Where things are
|
|
11
|
+
|
|
12
|
+
- Components: `app/components/ui/` (`UI::Button` is `app/components/ui/button.rb`)
|
|
13
|
+
- Their Stimulus controllers: `app/javascript/controllers/ui/`, registered as `ui--<name>`
|
|
14
|
+
- Tests: `test/components/ui/` (Minitest, `render_inline`)
|
|
15
|
+
- Lookbook previews: `test/components/previews/ui/`
|
|
16
|
+
- Design tokens: `app/assets/tailwind/velvetkit.css`
|
|
17
|
+
- Strings: `config/locales/velvetkit.en.yml`, under `ui.`
|
|
18
|
+
- Icons: `app/javascript/velvetkit_icons.ts`
|
|
19
|
+
|
|
20
|
+
## Rules
|
|
21
|
+
|
|
22
|
+
1. **Render, with slots.** Components take keyword arguments and fill their
|
|
23
|
+
parts through `with_*` slots in a block:
|
|
24
|
+
|
|
25
|
+
```erb
|
|
26
|
+
<%= render(UI::Card.new) do |card| %>
|
|
27
|
+
<% card.with_header do |header| %>
|
|
28
|
+
<% header.with_title { "Billing" } %>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% card.with_content { "…" } %>
|
|
31
|
+
<% end %>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
2. **Pass `class:` to adjust, not to restyle.** Your classes are merged with
|
|
35
|
+
the component's by tailwind_merge, and yours win on conflict. For a
|
|
36
|
+
different look, use a `variant:` or `size:` the component already has.
|
|
37
|
+
|
|
38
|
+
3. **Never hardcode colours.** Use the tokens: `bg-primary`,
|
|
39
|
+
`text-muted-foreground`, `border-input`, `bg-card`, `text-destructive`.
|
|
40
|
+
They switch with dark mode. Hex values and `blue-600` don't.
|
|
41
|
+
|
|
42
|
+
4. **Need a component the app doesn't have yet?** Add it from the kit rather
|
|
43
|
+
than writing one: `bin/rails generate velvetkit:add <name>`. It copies the
|
|
44
|
+
component with everything it depends on, and refreshes this file.
|
|
45
|
+
Available: {{available}}.
|
|
46
|
+
|
|
47
|
+
5. **Icons are Lucide names** (`UI::Icon.new(name: "calendar")`). An icon the
|
|
48
|
+
app hasn't used before must be added to `app/javascript/velvetkit_icons.ts`,
|
|
49
|
+
in both the import list and the `icons` object, or it renders empty.
|
|
50
|
+
|
|
51
|
+
6. **User-facing text goes through I18n**, as the components' own strings do.
|
|
52
|
+
|
|
53
|
+
7. **Changing a component is fine**, it's app code. Keep its test passing, and
|
|
54
|
+
update its Lookbook preview so the change can be seen.
|
|
55
|
+
|
|
56
|
+
8. **Links that look like buttons** use `UI::Link.new(href:, variant:, button_variant: true)`.
|
|
57
|
+
Without `button_variant: true` a link is styled as text.
|
|
58
|
+
|
|
59
|
+
## Components installed here
|
|
60
|
+
|
|
61
|
+
Each section below lists a component's arguments, slots and working
|
|
62
|
+
examples. The examples are the same templates its Lookbook previews render.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<div class="max-w-lg">
|
|
2
|
+
<h2 class="text-xl font-semibold mb-4">Frequently Asked Questions</h2>
|
|
3
|
+
<%= render(UI::Accordion.new(type: :single)) do |accordion| %>
|
|
4
|
+
<% faqs = [
|
|
5
|
+
{ q: "How do I reset my password?", a: "Go to Settings > Security and click 'Change Password'. You'll need to enter your current password and your new password." },
|
|
6
|
+
{ q: "Can I import data from another system?", a: "Yes, we import CSV and JSON files. Go to Admin > Data Import and follow the wizard to map your fields." },
|
|
7
|
+
{ q: "What integrations are available?", a: "Most tools connect through our API and webhooks, and the integrations page lists ready-made ones." },
|
|
8
|
+
{ q: "Is there a mobile app?", a: "Yes, for iOS and Android. Search for it in your phone's app store." },
|
|
9
|
+
{ q: "How do I contact support?", a: 'Email us at support@example.com or use the live chat widget in the bottom-right corner of your dashboard.' }
|
|
10
|
+
] %>
|
|
11
|
+
<% faqs.each_with_index do |faq, i| %>
|
|
12
|
+
<% accordion.with_item(value: "faq-#{i}") do |item| %>
|
|
13
|
+
<% item.with_trigger { faq[:q] } %>
|
|
14
|
+
<% item.with_content do %>
|
|
15
|
+
<p class="text-muted-foreground"><%= faq[:a].html_safe %></p>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<div class="max-w-md">
|
|
2
|
+
<%= render(UI::Accordion.new(type: :multiple, default_value: ["item-1", "item-3"])) do |accordion| %>
|
|
3
|
+
<% accordion.with_item(value: "item-1") do |item| %>
|
|
4
|
+
<% item.with_trigger { "Features" } %>
|
|
5
|
+
<% item.with_content do %>
|
|
6
|
+
<ul class="list-disc list-inside text-sm text-muted-foreground space-y-1">
|
|
7
|
+
<li>Projects and tasks</li>
|
|
8
|
+
<li>Time tracking</li>
|
|
9
|
+
<li>Grade reporting</li>
|
|
10
|
+
<li>Client portal</li>
|
|
11
|
+
</ul>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% accordion.with_item(value: "item-2") do |item| %>
|
|
15
|
+
<% item.with_trigger { "Pricing" } %>
|
|
16
|
+
<% item.with_content do %>
|
|
17
|
+
<p class="text-sm text-muted-foreground">We offer flexible pricing plans for teams of all sizes. Contact our sales team for a custom quote.</p>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% accordion.with_item(value: "item-3") do |item| %>
|
|
21
|
+
<% item.with_trigger { "Support" } %>
|
|
22
|
+
<% item.with_content do %>
|
|
23
|
+
<p class="text-sm text-muted-foreground">24/7 support via email and chat. Premium plans include dedicated account managers and phone support.</p>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<div class="max-w-md">
|
|
2
|
+
<%= render(UI::Accordion.new(type: :single, default_value: "item-1")) do |accordion| %>
|
|
3
|
+
<% accordion.with_item(value: "item-1") do |item| %>
|
|
4
|
+
<% item.with_trigger { "What is a workspace?" } %>
|
|
5
|
+
<% item.with_content do %>
|
|
6
|
+
<p class="text-muted-foreground">A workspace holds your team, projects and billing in one place. Everyone you invite sees the same projects.</p>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% accordion.with_item(value: "item-2") do |item| %>
|
|
10
|
+
<% item.with_trigger { "How do I get started?" } %>
|
|
11
|
+
<% item.with_content do %>
|
|
12
|
+
<p class="text-muted-foreground">Create an account, name your workspace and invite your team. A short setup guide walks you through each step.</p>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% accordion.with_item(value: "item-3") do |item| %>
|
|
16
|
+
<% item.with_trigger { "Is my data secure?" } %>
|
|
17
|
+
<% item.with_content do %>
|
|
18
|
+
<p class="text-muted-foreground">Yes. We use industry-standard encryption and follow GDPR to ensure your data is protected.</p>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class UI::AccordionPreview < ViewComponent::Preview
|
|
4
|
+
# @label Playground
|
|
5
|
+
# @param type select { choices: [single, multiple] }
|
|
6
|
+
def playground(type: :single)
|
|
7
|
+
render(UI::Accordion.new(type: type.to_sym, default_value: "item-1")) do |accordion|
|
|
8
|
+
accordion.with_item(value: "item-1") do |item|
|
|
9
|
+
item.with_trigger { "Is it accessible?" }
|
|
10
|
+
item.with_content do
|
|
11
|
+
tag.p(class: "text-muted-foreground") { "Yes. It adheres to the WAI-ARIA design pattern." }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
accordion.with_item(value: "item-2") do |item|
|
|
15
|
+
item.with_trigger { "Is it styled?" }
|
|
16
|
+
item.with_content do
|
|
17
|
+
tag.p(class: "text-muted-foreground") { "Yes. It comes with default styles that match the other components' aesthetic." }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @label Single (one open at a time)
|
|
24
|
+
def single; render_with_template; end
|
|
25
|
+
|
|
26
|
+
# @label Multiple (independent items)
|
|
27
|
+
def multiple; render_with_template; end
|
|
28
|
+
|
|
29
|
+
# @label FAQ Style
|
|
30
|
+
def faq_style; render_with_template; end
|
|
31
|
+
end
|