├── .editorconfig ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .rspec ├── .ruby-version ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ └── stylesheets │ │ ├── application.css │ │ ├── application.tailwind.css │ │ ├── lambda.light.css │ │ └── shadcn.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── components │ └── shadcn │ │ ├── form_builder.rb │ │ └── select_component.rb ├── controllers │ ├── application_controller.rb │ ├── components_controller.rb │ ├── concerns │ │ └── .keep │ ├── documentation_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ ├── components │ │ ├── accordion_helper.rb │ │ ├── alert_dialog_helper.rb │ │ ├── alert_helper.rb │ │ ├── badge_helper.rb │ │ ├── button_helper.rb │ │ ├── card_helper.rb │ │ ├── checkbox_helper.rb │ │ ├── collapsible_helper.rb │ │ ├── combobox_helper.rb │ │ ├── date_picker_helper.rb │ │ ├── dialog_helper.rb │ │ ├── dropdown_menu_helper.rb │ │ ├── dropzone_helper.rb │ │ ├── filter_helper.rb │ │ ├── forms_helper.rb │ │ ├── hover_card_helper.rb │ │ ├── input_helper.rb │ │ ├── label_helper.rb │ │ ├── list_helper.rb │ │ ├── popover_helper.rb │ │ ├── progress_helper.rb │ │ ├── select_helper.rb │ │ ├── separator_helper.rb │ │ ├── sheet_helper.rb │ │ ├── skeleton_helper.rb │ │ ├── slider_helper.rb │ │ ├── switch_helper.rb │ │ ├── table_helper.rb │ │ ├── tabs_helper.rb │ │ ├── textarea_helper.rb │ │ ├── toast_helper.rb │ │ ├── toggle_helper.rb │ │ └── tooltip_helper.rb │ ├── components_helper.rb │ ├── documentation_helper.rb │ └── examples_helper.rb ├── javascript │ ├── application.js │ ├── controllers │ │ ├── application.js │ │ ├── hello_controller.js │ │ ├── highlight_controller.js │ │ ├── index.js │ │ ├── theme_controller.js │ │ └── ui │ │ │ ├── accordion_controller.js │ │ │ ├── checkbox_controller.js │ │ │ ├── collapsible_controller.js │ │ │ ├── date-picker_controller.js │ │ │ ├── dialog_controller.js │ │ │ ├── dropdown_controller.js │ │ │ ├── dropzone_controller.js │ │ │ ├── filter_controller.js │ │ │ ├── hover-card_controller.js │ │ │ ├── popover_controller.js │ │ │ ├── select_controller.js │ │ │ ├── sheet_controller.js │ │ │ ├── slider_controller.js │ │ │ ├── switch_controller.js │ │ │ ├── tabs_controller.js │ │ │ ├── toast_controller.js │ │ │ ├── toggle_controller.js │ │ │ ├── tooltip_controller.js │ │ │ └── transition_controller.js │ └── utils │ │ ├── bodyScrollLock.js │ │ └── iso_date.js ├── jobs │ └── application_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── concerns │ │ └── .keep │ └── user.rb └── views │ ├── application │ └── index.html.erb │ ├── components │ └── ui │ │ ├── _accordion.html.erb │ │ ├── _alert.html.erb │ │ ├── _alert_dialog.html.erb │ │ ├── _button.html.erb │ │ ├── _card.html.erb │ │ ├── _checkbox.html.erb │ │ ├── _collapsible.html.erb │ │ ├── _combobox.html.erb │ │ ├── _command.html.erb │ │ ├── _date_picker.html.erb │ │ ├── _dialog.html.erb │ │ ├── _dropdown_menu.html.erb │ │ ├── _dropzone.html.erb │ │ ├── _filter.html.erb │ │ ├── _hover_card.html.erb │ │ ├── _input.html.erb │ │ ├── _label.html.erb │ │ ├── _list.html.erb │ │ ├── _popover.html.erb │ │ ├── _progress.html.erb │ │ ├── _sheet.html.erb │ │ ├── _skeleton.html.erb │ │ ├── _slider.html.erb │ │ ├── _switch.html.erb │ │ ├── _tabs.html.erb │ │ ├── _textarea.html.erb │ │ ├── _toast.html.erb │ │ ├── _toggle.html.erb │ │ ├── _tooltip.html.erb │ │ ├── shared │ │ ├── _backdrop.html.erb │ │ └── _menu_item.html.erb │ │ ├── svg │ │ └── _check.html.erb │ │ └── tabs │ │ ├── _panel.html.erb │ │ └── _tab.html.erb │ ├── documentation │ ├── about.html.md │ ├── generators.html.md │ ├── helpers.html.md │ ├── index.html.erb.bak │ ├── index.html.md │ ├── installation.html.md │ └── javascript.html.md │ ├── examples │ ├── authentication │ │ └── index.html.erb │ └── components │ │ ├── accordion.html.erb │ │ ├── accordion │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _block.html.erb │ │ │ ├── _description.html.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── alert-dialog.html.erb │ │ ├── alert-dialog │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── alert.html.erb │ │ ├── alert │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _attention.erb │ │ │ ├── _destructive.erb │ │ │ ├── _info.erb │ │ │ ├── _no_icon.erb │ │ │ ├── _preview.erb │ │ │ ├── _success.erb │ │ │ └── _usage.erb │ │ ├── badge.html.erb │ │ ├── badge │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── button.html.erb │ │ ├── button │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── card.html.erb │ │ ├── card │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _form.erb │ │ │ ├── _notifications.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── checkbox.html.erb │ │ ├── checkbox │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── collapsible.html.erb │ │ ├── collapsible │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── combobox.html.erb │ │ ├── combobox │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── date-picker.html.erb │ │ ├── date-picker │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── dialog.html.erb │ │ ├── dialog │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _form.erb │ │ │ ├── _notifications.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── dropdown-menu.html.erb │ │ ├── dropdown-menu │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── dropzone.html.erb │ │ ├── dropzone │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── filter.html.erb │ │ ├── filter │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _icon.html.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── forms.html.erb │ │ ├── forms │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── hover-card.html.erb │ │ ├── hover-card │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── input.html.erb │ │ ├── input │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _borderless.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── label.html.erb │ │ ├── label │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── popover.html.erb │ │ ├── popover │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _form.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── progress.html.erb │ │ ├── progress │ │ ├── _usage.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── select.html.erb │ │ ├── select │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── separator.html.erb │ │ ├── separator │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _fancy.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── sheet.html.erb │ │ ├── sheet │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _mobile_menu.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── skeleton.html.erb │ │ ├── skeleton │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── slider.html.erb │ │ ├── slider │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── switch.html.erb │ │ ├── switch │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── table.html.erb │ │ ├── table │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── tabs.html.erb │ │ ├── tabs │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _account.html.erb │ │ │ ├── _password.html.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── textarea.html.erb │ │ ├── textarea │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── toast.html.erb │ │ ├── toast │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _destructive.erb │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── toggle.html.erb │ │ ├── toggle │ │ ├── _usage.html.erb │ │ └── code │ │ │ ├── _preview.erb │ │ │ └── _usage.erb │ │ ├── tooltip.html.erb │ │ └── tooltip │ │ ├── _usage.html.erb │ │ └── code │ │ ├── _preview.erb │ │ └── _usage.erb │ └── layouts │ ├── application.html.erb │ ├── component.html.erb │ ├── documentation.html.erb │ ├── documentation │ ├── _breadcrumb.html.erb │ ├── _component_header.html.erb │ ├── _examples.html.erb │ └── _preview.html.erb │ ├── mailer.html.erb │ ├── mailer.text.erb │ └── shared │ ├── _components.html.erb │ ├── _header.html.erb │ └── _sidebar.html.erb ├── bin ├── bundle ├── console ├── debug ├── dev ├── importmap ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── importmap.rb ├── initializers │ ├── assets.rb │ ├── content_security_policy.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── markdown.rb │ └── permissions_policy.rb ├── locales │ └── en.yml ├── puma.rb ├── render.yml ├── routes.rb ├── shadcn.tailwind.js ├── storage.yml └── tailwind.config.js ├── db └── seeds.rb ├── lib ├── assets │ └── .keep ├── components.json ├── generators │ └── shadcn-ui_generator.rb ├── shadcn-ui │ ├── shadcn-ui.rb │ └── version.rb └── tasks │ └── .keep ├── log └── .keep ├── package-lock.json ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── accordion.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── og.jpg └── robots.txt ├── shadcn-ui.gemspec ├── sig └── shadcn-ui.rbs ├── spec ├── generators │ ├── alert_dialog_generator_spec.rb │ ├── dialog_generator_spec.rb │ └── shadcn-ui_generator_spec.rb ├── rails_helper.rb └── spec_helper.rb ├── storage └── .keep ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep └── vendor ├── .keep └── javascript └── .keep /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-* 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore pidfiles, but keep the directory. 21 | /tmp/pids/* 22 | !/tmp/pids/ 23 | !/tmp/pids/.keep 24 | 25 | # Ignore uploaded files in development. 26 | /storage/* 27 | !/storage/.keep 28 | /tmp/storage/* 29 | !/tmp/storage/ 30 | !/tmp/storage/.keep 31 | 32 | /public/assets 33 | 34 | # Ignore master key for decrypting credentials and more. 35 | /config/master.key 36 | 37 | /app/assets/builds/* 38 | !/app/assets/builds/.keep 39 | node_modules 40 | /.bundle/ 41 | /.yardoc 42 | /_yardoc/ 43 | /coverage/ 44 | /pkg/ 45 | /spec/reports/ 46 | /tmp/ 47 | 48 | # rspec failure tracking 49 | .rspec_status 50 | bin/deploy 51 | .env 52 | # Rubymine 53 | /.idea/* 54 | 55 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-* 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore pidfiles, but keep the directory. 21 | /tmp/pids/* 22 | !/tmp/pids/ 23 | !/tmp/pids/.keep 24 | 25 | # Ignore uploaded files in development. 26 | /storage/* 27 | !/storage/.keep 28 | /tmp/storage/* 29 | !/tmp/storage/ 30 | !/tmp/storage/.keep 31 | 32 | /public/assets 33 | 34 | # Ignore master key for decrypting credentials and more. 35 | /config/master.key 36 | 37 | /app/assets/builds/* 38 | !/app/assets/builds/.keep 39 | 40 | exa-taildash 41 | node_modules 42 | yarn-error.log 43 | node_modules 44 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "bracketSameLine": false, 5 | "printWidth": 100, 6 | "singleAttributePerLine": true, 7 | "proseWrap": "always", 8 | "overrides": [ 9 | { 10 | "files": "*.erb", 11 | "options": { 12 | "parser": "html" 13 | } 14 | }, 15 | { 16 | "files": "erb", 17 | "options": { 18 | "parser": "html" 19 | } 20 | }, 21 | { 22 | "files": "*.html.erb", 23 | "options": { 24 | "parser": "html" 25 | } 26 | }, 27 | { 28 | "files": "*.md.html", 29 | "options": { 30 | "parser": "markdown" 31 | } 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.2.2 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[html.erb]": { 3 | "editor.defaultFormatter": "aliariff.vscode-erb-beautify" 4 | }, 5 | "[erb]": { 6 | "editor.defaultFormatter": "manuelpuyol.erb-linter" 7 | }, 8 | "[rbs]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.1 / 2023-07-09 2 | 3 | ## Enhancements: 4 | 5 | - Began to add the gem structure for the gem. Pull request 6 | [#1](https://github.com/aviflombaum/shadcn-rails/pull/1) 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 shadcn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | css: bin/rails tailwindcss:watch 3 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviflombaum/shadcn-rails/d39354ce32e527061d4fbfe4958e6025626cdece/app/assets/builds/.keep -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | //= link_tree ../../javascript .js 4 | //= link_tree ../../../vendor/javascript .js 5 | //= link_tree ../builds 6 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviflombaum/shadcn-rails/d39354ce32e527061d4fbfe4958e6025626cdece/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.tailwind.css: -------------------------------------------------------------------------------- 1 | @import "shadcn.css"; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer base { 8 | .container { 9 | margin-left: auto; 10 | margin-right: auto; 11 | padding-left: 2rem; 12 | padding-right: 2rem; 13 | width: 100%; 14 | } 15 | 16 | :root { 17 | --pink: 301 98% 50% 0.88; 18 | } 19 | } 20 | 21 | article.documentation h1 { 22 | @apply scroll-m-20 text-4xl font-bold tracking-tight; 23 | } 24 | 25 | article.documentation p + h1 { 26 | @apply mt-12; 27 | } 28 | article.documentation h2 { 29 | @apply scroll-m-20 text-3xl font-bold tracking-tight my-8; 30 | } 31 | 32 | article.documentation h3 { 33 | @apply scroll-m-20 text-2xl font-bold tracking-tight my-4; 34 | } 35 | 36 | article.documentation h4 { 37 | @apply scroll-m-20 text-xl font-bold tracking-tight my-2; 38 | } 39 | 40 | article.documentation subtitle { 41 | @apply text-lg text-muted-foreground; 42 | } 43 | article.documentation p { 44 | @apply leading-7 [&:not(:first-child)]:mt-6; 45 | } 46 | 47 | article.documentation :where(code):not(:where([class~="not-prose"] *)):before { 48 | content: ""; 49 | } 50 | 51 | article.documentation :where(code):not(:where([class~="not-prose"] *)):after { 52 | content: ""; 53 | } 54 | 55 | article.documentation p a { 56 | @apply underline hover:text-pink; 57 | } 58 | 59 | article.documentation pre { 60 | @apply max-h-[650px] overflow-x-auto rounded-lg border bg-zinc-950 dark:bg-zinc-900 text-white py-4 px-8 rounded-lg my-4; 61 | } 62 | 63 | article.documentation p > code { 64 | @apply text-white bg-black p-1 rounded; 65 | } 66 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/components/shadcn/select_component.rb: -------------------------------------------------------------------------------- 1 | class Shadcn::SelectComponent 2 | include ComponentsHelper 3 | attr_reader :name, :selected, :view_context 4 | 5 | def initialize(name:, view_context:, selected: nil, **options, &block) 6 | @name = name 7 | @view_context = view_context 8 | @selected = selected 9 | @options = options 10 | @content = view_context.capture(self, &block) if block 11 | end 12 | 13 | def option(value:, label: nil, &block) 14 | content = label || view_context.capture(&block) 15 | option_options = {value: value} 16 | option_options[:selected] = "selected" if value == selected 17 | view_context.content_tag :option, content, option_options 18 | end 19 | 20 | def call 21 | view_context.content_tag :select, @content, name: name, class: tw("rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", @options[:class]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | def index 3 | end 4 | 5 | def examples 6 | render "examples/authentication/index" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/components_controller.rb: -------------------------------------------------------------------------------- 1 | class ComponentsController < ActionController::Base 2 | layout "component" 3 | 4 | def show 5 | @user = User.new # REFACTOR: For the forms example 6 | render "examples/components/#{params[:component]}" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviflombaum/shadcn-rails/d39354ce32e527061d4fbfe4958e6025626cdece/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/documentation_controller.rb: -------------------------------------------------------------------------------- 1 | class DocumentationController < ActionController::Base 2 | layout "documentation" 3 | def index 4 | end 5 | 6 | def show 7 | render "documentation/#{params[:id]}" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | layout "component" 3 | 4 | def create 5 | params[:component] = "forms" 6 | @user = User.new(user_params) 7 | if @user.valid? 8 | # Toast message 9 | else 10 | render "examples/components/forms", status: 422 11 | end 12 | end 13 | 14 | private 15 | 16 | def user_params 17 | params.require(:user).permit(:email, :password) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def page_title 3 | @page_title = "" 4 | if request.path.include?("/docs/components") 5 | component_name = params[:component].to_s.titleize 6 | @page_title << "#{component_name} - " if component_name.present? 7 | end 8 | @page_title << "shadcn/ui on Rails" 9 | 10 | set_meta_tags( 11 | title: @page_title 12 | ) 13 | 14 | @page_title 15 | end 16 | 17 | def sidebar_link(text, path) 18 | classes = "group flex w-full items-center rounded-md border border-transparent px-2 py-1 hover:underline" 19 | classes << if request.path == path 20 | " text-foreground font-bold" 21 | else 22 | " text-muted-foreground" 23 | end 24 | link_to text, path, class: classes 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/helpers/components/accordion_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::AccordionHelper 2 | def accordion_title(&block) 3 | content_for :title, capture(&block), flush: true 4 | end 5 | 6 | def accordion_description(&block) 7 | content_for :description, capture(&block), flush: true 8 | end 9 | 10 | def render_accordion(title: nil, description: nil, &block) 11 | if title && !description 12 | content_for :description, capture(&block), flush: true 13 | elsif !title && !description 14 | capture(&block) 15 | end 16 | render "components/ui/accordion", title: title, description: description 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/helpers/components/alert_dialog_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::AlertDialogHelper 2 | def render_alert_dialog(**options, &block) 3 | content = capture(&block) if block 4 | render "components/ui/alert_dialog", content: content, **options 5 | end 6 | 7 | def alert_dialog_trigger(&block) 8 | content_for :alert_dialog_trigger, capture(&block), flush: true 9 | end 10 | 11 | def alert_dialog_content(&block) 12 | content_for :alert_dialog_content, capture(&block), flush: true 13 | end 14 | 15 | def alert_dialog_continue(&block) 16 | content_for :alert_dialog_continue, capture(&block), flush: true 17 | end 18 | 19 | def alert_dialog_cancel(&block) 20 | content_for :alert_dialog_cancel, capture(&block), flush: true 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/helpers/components/alert_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::AlertHelper 2 | def render_alert(title:, description: nil, variant: :default, icon: true, &block) 3 | alert_classes = case variant.to_sym 4 | when :default 5 | "[&>svg]:text-foreground bg-background text-foreground" 6 | when :error, :danger, :alert, :destructive 7 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive" 8 | when :success 9 | "border-success/50 text-success dark:border-success [&>svg]:text-success" 10 | when :info 11 | "border-info/50 text-info dark:border-info [&>svg]:text-info" 12 | when :attention 13 | "border-attention/50 text-attention dark:border-attention [&>svg]:text-attention" 14 | end 15 | content = (capture(&block) if block) || description 16 | render "components/ui/alert", title:, content:, alert_classes:, variant:, icon: 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/helpers/components/badge_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::BadgeHelper 2 | def render_badge(label = "", data: "", text: "", variant: :default, **options) 3 | badge_classes = " inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 " 4 | variant_classes = case variant.to_sym 5 | when :default 6 | ComponentsHelper::PRIMARY_CLASSES 7 | when :secondary 8 | ComponentsHelper::SECONDARY_CLASSES 9 | when :error, :danger, :alert, :destructive 10 | ComponentsHelper::DESTRUCTIVE_CLASSES 11 | when :outline 12 | ComponentsHelper::OUTLINE_CLASSES 13 | when :ghost 14 | ComponentsHelper::GHOST_CLASSES 15 | end 16 | badge_classes << " #{variant_classes}" 17 | text = label if label.present? 18 | 19 | content_tag :div, class: badge_classes do 20 | text 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/helpers/components/button_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::ButtonHelper 2 | def render_button(label = "", text: nil, variant: :default, as: :button, href: nil, data: {}, **options, &block) 3 | button_classes = " inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2 " 4 | variant_classes = case variant.to_sym 5 | when :default 6 | " bg-primary text-primary-foreground hover:bg-primary/90 " 7 | when :secondary 8 | " bg-secondary text-secondary-foreground hover:bg-secondary/80 " 9 | when :error, :danger, :alert, :destructive 10 | " bg-destructive text-destructive-foreground hover:bg-destructive/90 " 11 | when :outline 12 | " border border-input bg-background hover:bg-accent hover:text-accent-foreground" 13 | when :ghost 14 | " hover:bg-accent hover:text-accent-foreground " 15 | end 16 | button_classes = tw(button_classes, variant_classes, options[:class]) 17 | 18 | text = label if label.present? 19 | text = capture(&block) if block 20 | render "components/ui/button", text:, button_classes:, as:, href:, data:, **options 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/helpers/components/card_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::CardHelper 2 | def render_card(title: nil, subtitle: nil, body: nil, footer: nil, **options, &block) 3 | render "components/ui/card", title: title, subtitle: subtitle, footer: footer, body: (block ? capture(&block) : body), block:, options: options 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/components/checkbox_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::CheckboxHelper 2 | def render_checkbox(label:, name:, **options) 3 | render "components/ui/checkbox", name: name, label: label, options: options 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/components/collapsible_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::CollapsibleHelper 2 | def collapsible_preview(&block) 3 | content_for :preview, capture(&block) if block 4 | end 5 | 6 | def collapsible_body(&block) 7 | content_for :body, capture(&block) if block 8 | end 9 | 10 | def render_collapsible(**options, &block) 11 | content = capture(&block) if block 12 | render "components/ui/collapsible", content: content, **options 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/helpers/components/combobox_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::ComboboxHelper 2 | def render_combobox(items, &block) 3 | content = capture(&block) if block 4 | render "components/ui/combobox", items:, content: 5 | end 6 | 7 | def combobox_trigger(&block) 8 | content_for :trigger, capture(&block) if block 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/helpers/components/date_picker_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::DatePickerHelper 2 | def render_date_picker(name:, id: nil, value: 'Pick a date', **options) 3 | render partial: "components/ui/date_picker", locals: { 4 | name:, 5 | value:, 6 | id:, 7 | options: options 8 | } 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/helpers/components/dialog_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::DialogHelper 2 | def render_dialog(**options, &block) 3 | content = capture(&block) if block 4 | render "components/ui/dialog", content: content, options: options 5 | end 6 | 7 | def dialog_trigger(&block) 8 | content_for :dialog_trigger, capture(&block), flush: true 9 | end 10 | 11 | def dialog_content(&block) 12 | content_for :dialog_content, capture(&block), flush: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/helpers/components/dropdown_menu_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::DropdownMenuHelper 2 | def render_dropdown_menu(**options, &block) 3 | content = capture(&block) if block 4 | render "components/ui/dropdown_menu", content: content, **options 5 | end 6 | 7 | def dropdown_menu_trigger(&block) 8 | content_for :dropdown_menu_trigger, capture(&block), flush: true 9 | end 10 | 11 | def dropdown_menu_label(label = nil, &block) 12 | content_for :dropdown_menu_label, (label || capture(&block)), flush: true 13 | end 14 | 15 | def dropdown_menu_content(&block) 16 | content_for :dropdown_menu_content, capture(&block), flush: true 17 | end 18 | 19 | def dropdown_menu_item(label = nil, **options, &block) 20 | content = (label || capture(&block)) 21 | render "components/ui/shared/menu_item", content: content 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/helpers/components/dropzone_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::DropzoneHelper 2 | def render_dropzone 3 | render "components/ui/dropzone" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/components/filter_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::FilterHelper 2 | def filter_icon(&block) 3 | content_for :filter_icon, capture(&block), flush: true 4 | end 5 | 6 | def render_filter(items, **options, &block) 7 | content_for :filter_icon, "", flush: true 8 | content = capture(&block) if block 9 | input_class = content_for?(:filter_icon) ? "pl-1" : "" 10 | render "components/ui/filter", items: items, options: options, input_class: input_class, content: content 11 | end 12 | 13 | def list_item(value:, name:, selected:) 14 | "#{name}" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/helpers/components/forms_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::FormsHelper 2 | def render_form_with(**opts) 3 | form_with(**opts.merge(builder: Shadcn::FormBuilder)) do |form| 4 | yield form 5 | end 6 | end 7 | 8 | def render_form_for(obj, **opts) 9 | form_for(obj, **opts.merge(builder: Shadcn::FormBuilder), html: opts) do |form| 10 | yield form 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/helpers/components/hover_card_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::HoverCardHelper 2 | def render_hover_card(**options, &block) 3 | content = capture(&block) if block 4 | render "components/ui/hover_card", content: content, **options 5 | end 6 | 7 | def hover_card_trigger(&block) 8 | content_for :hover_card_trigger, capture(&block), flush: true 9 | end 10 | 11 | def hover_card_content(options = {}, &block) 12 | content_for :hover_card_content_class, options[:class], flush: true 13 | content_for :hover_card_content, capture(&block), flush: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/components/input_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::InputHelper 2 | def render_input(name:, label: false, id: nil, type: :text, value: nil, **options) 3 | options[:class] = "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm transition-colors ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 #{options[:class]} " 4 | options[:class] << case options[:variant] 5 | when :borderless 6 | " border-0 focus-visible:outline-none focus-visible:shadow-none focus-visible:ring-transparent" 7 | else 8 | "shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:border-muted" 9 | end 10 | options[:class] = tw(options[:class]) 11 | 12 | options.reverse_merge!( 13 | label: (options[:label] || false), 14 | required: (options[:required] || false), 15 | disabled: (options[:disabled] || false), 16 | readonly: (options[:readonly] || false), 17 | placeholder: (options[:placeholder] || ""), 18 | autocomplete: (options[:autocomplete] || ""), 19 | autocapitalize: (options[:autocapitalize] || nil), 20 | autocorrect: (options[:autocorrect] || nil), 21 | autofocus: (options[:autofocus] || nil) 22 | ) 23 | render partial: "components/ui/input", locals: { 24 | type:, 25 | label:, 26 | name:, 27 | value:, 28 | id:, 29 | options: options 30 | } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/helpers/components/label_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::LabelHelper 2 | def render_label(name:, label:, **options) 3 | render "components/ui/label", name: name, label: label, options: options 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/components/list_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::ListHelper 2 | def list_item(value:, name:, selected: false, as: :div) 3 | content_tag as, value, 4 | class: "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm 5 | outline-none aria-selected:bg-accent aria-selected:text-accent-foreground hover:bg-accent hover:text-accent-foreground 6 | data-[disabled]:pointer-events-none data-[disabled]:opacity-50", 7 | role: "option", 8 | data: {value:, selected:}, 9 | aria: {selected:} 10 | end 11 | 12 | def render_list(items, as: :div, **options) 13 | render "components/ui/list", items:, as:, **options 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/components/popover_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::PopoverHelper 2 | def render_popover(**options, &block) 3 | content = capture(&block) if block 4 | render "components/ui/popover", content: content, **options 5 | end 6 | 7 | def popover_trigger(&block) 8 | content_for :popover_trigger, capture(&block), flush: true 9 | end 10 | 11 | def popover_content(options = {}, &block) 12 | content_for :popover_content_class, options[:class], flush: true 13 | content_for :popover_content, capture(&block), flush: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/components/progress_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::ProgressHelper 2 | def render_progress(value:, **options) 3 | render "components/ui/progress", value: (100 - value), **options 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/components/select_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::SelectHelper 2 | def render_select(name:, **options, &block) 3 | component = Shadcn::SelectComponent.new(name: name, view_context: self, **options, &block) 4 | component.call 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/helpers/components/separator_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::SeparatorHelper 2 | def render_separator(options = {}) 3 | options = {class: "shrink-0 bg-border h-[1px] w-full #{options[:class]}"}.reverse_merge(options) 4 | content_tag :div, "", options 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/helpers/components/sheet_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::SheetHelper 2 | def render_sheet(**options, &block) 3 | options[:direction] ||= "left" 4 | options[:width] ||= "w-3/4" 5 | options[:state] ||= "closed" 6 | 7 | content_for :sheet_trigger, "", flush: true 8 | content_for :sheet_content, "", flush: true 9 | 10 | content = capture(&block) if block 11 | render "components/ui/sheet", content: content, options: options 12 | end 13 | 14 | def sheet_trigger(&block) 15 | content_for :sheet_trigger, capture(&block), flush: true 16 | end 17 | 18 | def sheet_content(&block) 19 | content_for :sheet_content, capture(&block), flush: true 20 | end 21 | 22 | def direction_class(direction) 23 | mappings = { 24 | left: "left-0", 25 | right: "right-0" 26 | } 27 | 28 | mappings[direction.to_sym] 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/helpers/components/skeleton_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::SkeletonHelper 2 | def render_skeleton 3 | render "components/ui/skeleton" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/components/slider_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::SliderHelper 2 | def render_slider(name:, value: 0, id: nil, **options) 3 | render "components/ui/slider", value:, id:, name:, options: 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/components/switch_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::SwitchHelper 2 | def render_switch(text, id:, name:, state: "unchecked", **options) 3 | render "components/ui/switch", text:, id:, name:, state:, options: 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/components/tabs_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::TabsHelper 2 | def render_tabs(&block) 3 | @_tabs = [] 4 | capture(&block) 5 | render "components/ui/tabs" 6 | end 7 | 8 | def tab_list(&block) 9 | content_for :tab_list, capture(&block), flush: true 10 | end 11 | 12 | def tab(title, **options) 13 | options[:id] ||= "tab_#{title.parameterize}" 14 | options[:state] = options[:active] ? "active" : "inactive" 15 | 16 | @_tabs << {title: title, id: options[:id]} 17 | render("components/ui/tabs/tab", title:, options:) 18 | end 19 | 20 | def tab_panels(&block) 21 | content_for :tab_panels, capture(&block), flush: true 22 | end 23 | 24 | def tab_panel(**options, &block) 25 | options[:state] = options[:active] ? "active" : "inactive" 26 | content_for :panel, capture(&block), flush: true 27 | render("components/ui/tabs/panel", options:) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/helpers/components/textarea_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::TextareaHelper 2 | def render_textarea(name:, label: false, id: nil, value: nil, **options) 3 | options.reverse_merge!(rows: 3, required: false, disabled: false, 4 | readonly: false, class: "", label: false, placeholder: "Type here...") 5 | render partial: "components/ui/textarea", locals: { 6 | label:, 7 | name:, 8 | value:, 9 | id:, 10 | options: options 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/helpers/components/toast_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::ToastHelper 2 | def render_toast(header: nil, description: nil, action: nil, class: nil, data: {}, variant: :default, **options, &block) 3 | options[:class] ||= "" 4 | options[:class] << " destructive group border-destructive bg-destructive text-destructive-foreground " if variant == :destructive 5 | 6 | render "components/ui/toast", header:, description:, action:, class:, data:, options: options 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/helpers/components/toggle_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::ToggleHelper 2 | def render_toggle(label = nil, **options, &block) 3 | content = label || capture(&block) 4 | render "components/ui/toggle", content: content, **options 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/helpers/components/tooltip_helper.rb: -------------------------------------------------------------------------------- 1 | module Components::TooltipHelper 2 | def render_tooltip(**options, &block) 3 | content = capture(&block) if block 4 | render "components/ui/tooltip", content: content, **options 5 | end 6 | 7 | def tooltip_trigger(&block) 8 | content_for :tooltip_trigger, capture(&block), flush: true 9 | end 10 | 11 | def tooltip_content(options = {}, &block) 12 | content_for :tooltip_content_class, options[:class], flush: true 13 | content_for :tooltip_content, capture(&block), flush: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/components_helper.rb: -------------------------------------------------------------------------------- 1 | require "tailwind_merge" 2 | 3 | module ComponentsHelper 4 | def tw(*classes) 5 | TailwindMerge::Merger.new.merge(classes.join(" ")) 6 | end 7 | 8 | PRIMARY_CLASSES = " bg-primary text-primary-foreground hover:bg-primary/80 " 9 | SECONDARY_CLASSES = " bg-secondary text-secondary-foreground hover:bg-secondary/80 " 10 | OUTLINE_CLASSES = " border border-input bg-background hover:bg-accent hover:text-accent-foreground " 11 | GHOST_CLASSES = " hover:bg-accent hover:text-accent-foreground " 12 | DESTRUCTIVE_CLASSES = " bg-destructive text-destructive-foreground hover:bg-destructive/90 " 13 | 14 | module Button 15 | PRIMARY = ComponentsHelper::PRIMARY_CLASSES 16 | SECONDARY = ComponentsHelper::SECONDARY_CLASSES 17 | OUTLINE = ComponentsHelper::OUTLINE_CLASSES 18 | GHOST = ComponentsHelper::GHOST_CLASSES 19 | DESTRUCTIVE = ComponentsHelper::DESTRUCTIVE_CLASSES 20 | end 21 | 22 | module Alert 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/helpers/documentation_helper.rb: -------------------------------------------------------------------------------- 1 | module DocumentationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/examples_helper.rb: -------------------------------------------------------------------------------- 1 | module ExamplesHelper 2 | def render_component_header(title:, description:) 3 | render "layouts/documentation/component_header", title:, description: 4 | end 5 | 6 | def render_example 7 | render "layouts/documentation/examples" 8 | end 9 | 10 | def render_preview 11 | render "layouts/documentation/preview" 12 | end 13 | 14 | def render_usage(name) 15 | render "examples/components/#{name}/usage" 16 | end 17 | 18 | def render_code_preview(name) 19 | render "examples/components/#{name}/code/preview" 20 | end 21 | 22 | def code_partial(name, language) 23 | component, partial = name.split("/") 24 | content_tag :pre, class: "code-sample py-4 px-4", data: {controller: "highlight"} do 25 | content_tag :code, class: "language-#{language}" do 26 | html_escape(File.read(Rails.root.join("app", "views", "examples", "components", "#{component}/code/_#{partial}.erb"))) 27 | end 28 | end 29 | end 30 | 31 | def code_sample(content = "", language:, &block) 32 | content_tag :pre, class: "code-sample px-4 my-2 pb-5 min-h-fit", data: {controller: "highlight"} do 33 | content_tag :code, class: "language-#{language}" do 34 | yield if block 35 | end 36 | end 37 | end 38 | 39 | def inline_code(content = nil, &block) 40 | content_tag :code, class: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono font-semibold" do 41 | content || yield(block) 42 | end 43 | end 44 | alias_method :code, :inline_code 45 | end 46 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus"; 2 | 3 | const application = Application.start(); 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false; 7 | window.Stimulus = application; 8 | 9 | export { application }; 10 | -------------------------------------------------------------------------------- /app/javascript/controllers/hello_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.element.textContent = "Hello World!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/javascript/controllers/highlight_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | import hljs from "highlight.js"; 3 | 4 | export default class extends Controller { 5 | connect() { 6 | hljs.highlightAll(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap under controllers/* 2 | 3 | import { application } from "controllers/application" 4 | 5 | // Eager load all controllers defined in the import map under controllers/**/*_controller 6 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 7 | eagerLoadControllersFrom("controllers", application) 8 | 9 | // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) 10 | // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" 11 | // lazyLoadControllersFrom("controllers", application) 12 | -------------------------------------------------------------------------------- /app/javascript/controllers/theme_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["toggleButton"]; 5 | 6 | connect() { 7 | this.loadThemePreference(); 8 | } 9 | 10 | toggle() { 11 | const isDarkMode = document.documentElement.classList.toggle("dark"); 12 | this.saveThemePreference(isDarkMode); 13 | } 14 | 15 | loadThemePreference() { 16 | const isDarkMode = localStorage.getItem("themePreference") === "true"; 17 | if (isDarkMode) { 18 | document.documentElement.classList.add("dark"); 19 | } 20 | } 21 | 22 | saveThemePreference(isDarkMode) { 23 | localStorage.setItem("themePreference", isDarkMode); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/checkbox_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | export default class extends Controller { 3 | connect() { 4 | this.button = this.element.querySelector("button"); 5 | this.checkmark = this.button.querySelector("span"); 6 | } 7 | 8 | toggle() { 9 | if (this.checkmark.classList.contains("hidden")) { 10 | this.checkmark.classList.remove("hidden"); 11 | this.button.dataset.state = "checked"; 12 | } else { 13 | this.checkmark.classList.add("hidden"); 14 | this.button.dataset.state = "unchecked"; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/collapsible_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["item"]; 5 | static classes = ["hidden"]; 6 | 7 | connect() { 8 | this.class = this.hasHiddenClass ? this.hiddenClass : "hidden"; 9 | } 10 | 11 | toggle() { 12 | this.itemTargets.forEach((item) => { 13 | item.classList.toggle(this.class); 14 | }); 15 | } 16 | 17 | show() { 18 | this.itemTargets.forEach((item) => { 19 | item.classList.remove(this.class); 20 | }); 21 | } 22 | 23 | hide() { 24 | this.itemTargets.forEach((item) => { 25 | item.classList.add(this.class); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/dropdown_controller.js: -------------------------------------------------------------------------------- 1 | // Inspired By: https://github.com/stimulus-components/stimulus-dropdown/blob/master/src/index.ts 2 | import UIPopover from "controllers/ui/popover_controller"; 3 | import { useTransition } from "https://ga.jspm.io/npm:stimulus-use@0.51.3/dist/index.js"; 4 | 5 | export default class extends UIPopover {} 6 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/dropzone_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class extends Controller { 4 | static targets = ["fileInput"]; 5 | connect() { 6 | this.element.addEventListener("dragover", this.preventDragDefaults); 7 | this.element.addEventListener("dragenter", this.preventDragDefaults); 8 | } 9 | 10 | disconnect() { 11 | this.element.removeEventListener("dragover", this.preventDragDefaults); 12 | this.element.removeEventListener("dragenter", this.preventDragDefaults); 13 | } 14 | 15 | preventDragDefaults(e) { 16 | e.preventDefault(); 17 | e.stopPropagation(); 18 | } 19 | 20 | trigger() { 21 | this.fileInputTarget.click(); 22 | } 23 | 24 | acceptFiles(event) { 25 | event.preventDefault(); 26 | const files = event.dataTransfer ? event.dataTransfer.files : event.target.files; 27 | [...files].forEach((file) => { 28 | this.uploadFile(file); 29 | }); 30 | } 31 | 32 | // Implement your own file upload strategy here... 33 | uploadFile(file) { 34 | console.log("Received file for upload: ", file); 35 | console.log("Implement your own file upload strategy here..."); 36 | // const formData = new FormData(); 37 | // formData.append("file", file); 38 | 39 | // fetch("/upload", { 40 | // method: "POST", 41 | // body: formData, 42 | // }) 43 | // .then((response) => response.json()) 44 | // .then((data) => { 45 | // console.log(data); 46 | // }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/filter_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class UIFilter extends Controller { 4 | static targets = ["source", "item"]; 5 | 6 | connect() {} 7 | 8 | filter(event) { 9 | let lowerCaseFilterTerm = this.sourceTarget.value.toLowerCase(); 10 | let regex = new RegExp("^" + lowerCaseFilterTerm); 11 | if (this.hasItemTarget) { 12 | this.itemTargets.forEach((el, i) => { 13 | let filterableKey = el.innerText.toLowerCase(); 14 | 15 | // Check for consecutive characters match using regex 16 | el.classList.toggle("hidden", !regex.test(filterableKey)); 17 | }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/hover-card_controller.js: -------------------------------------------------------------------------------- 1 | // Inspired by: https://github.com/excid3/tailwindcss-stimulus-components/blob/master/src/popover.js 2 | 3 | import { Controller } from "@hotwired/stimulus"; 4 | import { createPopper } from "https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js"; 5 | import { useDebounce, useHover } from "https://ga.jspm.io/npm:stimulus-use@0.51.3/dist/index.js"; 6 | 7 | export default class UIHoverCardController extends Controller { 8 | static debounces = ["mouseEnter", "mouseLeave"]; 9 | static targets = ["content", "wrapper", "trigger"]; 10 | 11 | connect() { 12 | useDebounce(this); 13 | useHover(this, { element: this.triggerTarget }); 14 | this.popperInstance = createPopper(this.triggerTarget, this.contentTarget, { 15 | placement: this.contentTarget.dataset.side || "bottom", 16 | modifiers: [ 17 | { 18 | name: "offset", 19 | options: { 20 | offset: [0, 8], 21 | }, 22 | }, 23 | ], 24 | }); 25 | } 26 | 27 | mouseEnter() { 28 | this.popperInstance.update(); 29 | this.contentTarget.dataset.state = "open"; 30 | this.contentTarget.classList.remove("hidden"); 31 | } 32 | 33 | mouseLeave() { 34 | this.popperInstance.update(); 35 | this.contentTarget.dataset.state = "closed"; 36 | this.contentTarget.classList.add("hidden"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/popover_controller.js: -------------------------------------------------------------------------------- 1 | // Inspired by: https://github.com/excid3/tailwindcss-stimulus-components/blob/master/src/popover.js 2 | 3 | import { Controller } from "@hotwired/stimulus"; 4 | import { createPopper } from "https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js"; 5 | import { useClickOutside } from "https://ga.jspm.io/npm:stimulus-use@0.51.3/dist/index.js"; 6 | 7 | export default class UIPopover extends Controller { 8 | static values = { 9 | dismissAfter: Number, 10 | }; 11 | static targets = ["content", "wrapper", "trigger"]; 12 | 13 | connect() { 14 | useClickOutside(this); 15 | this.popperInstance = createPopper(this.triggerTarget, this.contentTarget, { 16 | placement: this.contentTarget.dataset.side || "bottom", 17 | modifiers: [ 18 | { 19 | name: "offset", 20 | options: { 21 | offset: [0, 8], 22 | }, 23 | }, 24 | ], 25 | }); 26 | } 27 | 28 | // Show the popover 29 | show() { 30 | this.contentTarget.classList.remove("hidden"); 31 | this.contentTarget.dataset.state = "open"; 32 | } 33 | 34 | // Hide the popover 35 | hide() { 36 | this.contentTarget.classList.add("hidden"); 37 | this.contentTarget.dataset.state = "closed"; 38 | } 39 | 40 | clickOutside(event) { 41 | this.hide(); 42 | } 43 | 44 | // Toggle the popover on demand 45 | toggle(event) { 46 | this.popperInstance.update(); 47 | if (this.contentTarget.classList.contains("hidden")) { 48 | this.show(); 49 | 50 | if (this.hasDismissAfterValue) { 51 | setTimeout(() => { 52 | this.hide(); 53 | }, this.dismissAfterValue); 54 | } 55 | } else { 56 | this.hide(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/sheet_controller.js: -------------------------------------------------------------------------------- 1 | import UIDialog from "controllers/ui/dialog_controller"; 2 | import "https://ga.jspm.io/npm:@kanety/stimulus-static-actions@1.0.1/dist/index.modern.js"; 3 | 4 | export default class extends UIDialog { 5 | // Handles a button triggering the sheet in a different 6 | // controller instance 7 | // REFACTOR: This is the toggle method in dialog_controller with dom elements 8 | // instead of targets. Update the method there to receive dom elements and this 9 | // can be refactored to use those methods instead of reimplementing. 10 | toggleOutlet() { 11 | const sheetTarget = document.querySelector(this.element.dataset.UiSheetOutlet); 12 | const dialogTarget = sheetTarget.querySelector("[data-ui--sheet-target='dialog']"); 13 | const modalTarget = sheetTarget.querySelector("[data-ui--sheet-target='modal']"); 14 | const contentTarget = sheetTarget.querySelector("[data-ui--sheet-target='content']"); 15 | const visible = dialogTarget.dataset.state == "closed" ? false : true; 16 | const mainTarget = document.body; 17 | if (!visible) { 18 | document.body.classList.add("overflow-hidden"); 19 | contentTarget.classList.add("overflow-y-scroll", "h-full"); 20 | dialogTarget.classList.remove("hidden"); 21 | dialogTarget.dataset.state = "open"; 22 | modalTarget.classList.remove("hidden"); 23 | modalTarget.dataset.state = "open"; 24 | } else { 25 | document.body.classList.remove("overflow-hidden"); 26 | contentTarget.classList.remove("overflow-y-scroll", "h-full"); 27 | dialogTarget.classList.add("hidden"); 28 | dialogTarget.dataset.state = "closed"; 29 | modalTarget.classList.add("hidden"); 30 | modalTarget.dataset.state = "closed"; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/slider_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | 3 | export default class UISliderController extends Controller { 4 | updateRange() { 5 | const input = this.element; 6 | const min = input.min; 7 | const max = input.max; 8 | const val = input.value; 9 | 10 | const fillRatio = parseInt(((val - min) * 100) / (max - min)); 11 | input.style = `background-size: ${fillRatio}% 100%`; 12 | input.setAttribute("value", fillRatio); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/switch_controller.js: -------------------------------------------------------------------------------- 1 | // Inspired by: https://github.com/excid3/tailwindcss-stimulus-components/blob/master/src/popover.js 2 | 3 | import { Controller } from "@hotwired/stimulus"; 4 | 5 | export default class UIToggleController extends Controller { 6 | connect() {} 7 | 8 | toggle() { 9 | const button = this.element.querySelector("button"); 10 | const span = this.element.querySelector("span"); 11 | const input = this.element.querySelector("input[type='hidden']"); 12 | 13 | if (this.element.dataset.state == "checked") { 14 | input.value = "unchecked"; 15 | button.dataset.state = "unchecked"; 16 | span.dataset.state = "unchecked"; 17 | this.element.dataset.state = "unchecked"; 18 | } else { 19 | input.value = "checked"; 20 | button.dataset.state = "checked"; 21 | span.dataset.state = "checked"; 22 | this.element.dataset.state = "checked"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/toggle_controller.js: -------------------------------------------------------------------------------- 1 | // Inspired by: https://github.com/excid3/tailwindcss-stimulus-components/blob/master/src/popover.js 2 | 3 | import { Controller } from "@hotwired/stimulus"; 4 | 5 | export default class UIToggleController extends Controller { 6 | connect() {} 7 | 8 | toggle() { 9 | if (this.element.dataset.state == "on") { 10 | this.element.dataset.state = "off"; 11 | } else { 12 | this.element.dataset.state = "on"; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/tooltip_controller.js: -------------------------------------------------------------------------------- 1 | import UIHoverCardController from "controllers/ui/hover-card_controller"; 2 | 3 | export default class UITooltipController extends UIHoverCardController {} 4 | -------------------------------------------------------------------------------- /app/javascript/controllers/ui/transition_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus"; 2 | import { useTransition } from "https://ga.jspm.io/npm:stimulus-use@0.51.3/dist/index.js"; 3 | 4 | export default class extends Controller { 5 | connect() { 6 | useTransition(this); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aviflombaum/shadcn-rails/d39354ce32e527061d4fbfe4958e6025626cdece/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User 2 | include ActiveModel::API 3 | include ActiveModel::SecurePassword 4 | 5 | attr_accessor :email, :password_digest 6 | 7 | has_secure_password 8 | validates :email, presence: true 9 | end 10 | -------------------------------------------------------------------------------- /app/views/components/ui/_accordion.html.erb: -------------------------------------------------------------------------------- 1 |
<%= subtitle %>
8 | <% end %> 9 |