diff --git a/Gemfile b/Gemfile index cfa068b..d3cc81c 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,6 @@ source 'https://rubygems.org' ruby file: '.ruby-version' gem 'bootsnap', require: false -gem 'bootstrap', '~> 5.3' gem 'castle-rb', '~> 8.1' gem 'devise', '~> 5.0' gem 'dotenv-rails' @@ -15,10 +14,10 @@ gem 'omniauth-twitter' gem 'puma', '~> 6.4' gem 'rails', '~> 8.1.3' gem 'responders' -gem 'sassc-rails' gem 'simple_form' gem 'sprockets-rails' gem 'sqlite3', '~> 2.1' +gem 'tailwindcss-rails', '~> 3.3' group :development, :test do gem 'byebug' diff --git a/Gemfile.lock b/Gemfile.lock index 15795b0..67e6933 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,8 +83,6 @@ GEM bindex (0.8.1) bootsnap (1.24.6) msgpack (~> 1.2) - bootstrap (5.3.8) - popper_js (>= 2.11.8, < 3) builder (3.3.0) byebug (13.0.0) reline (>= 0.6.0) @@ -116,8 +114,6 @@ GEM railties (>= 6.1.0) faker (3.8.0) i18n (>= 1.8.11, < 2) - ffi (1.17.4) - ffi (1.17.4-arm64-darwin) globalid (1.3.0) activesupport (>= 6.1) hamlit (4.0.0) @@ -170,8 +166,12 @@ GEM nokogiri (1.19.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nokogiri (1.19.3-aarch64-linux-gnu) + racc (~> 1.4) nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) oauth (1.1.5) auth-sanitizer (~> 0.1, >= 0.1.3) base64 (~> 0.1) @@ -199,7 +199,6 @@ GEM omniauth-oauth (~> 1.1) rack orm_adapter (0.5.0) - popper_js (2.11.8) pp (0.6.3) prettyprint prettyprint (0.2.0) @@ -283,14 +282,6 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.7) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt securerandom (0.4.1) simple_form (5.4.1) actionpack (>= 7.0) @@ -314,8 +305,17 @@ GEM sprockets (>= 3.0.0) sqlite3 (2.9.4) mini_portile2 (~> 2.8.0) + sqlite3 (2.9.4-aarch64-linux-gnu) sqlite3 (2.9.4-arm64-darwin) + sqlite3 (2.9.4-x86_64-linux-gnu) stringio (3.2.0) + tailwindcss-rails (3.3.2) + railties (>= 7.0.0) + tailwindcss-ruby (~> 3.0) + tailwindcss-ruby (3.4.19) + tailwindcss-ruby (3.4.19-aarch64-linux) + tailwindcss-ruby (3.4.19-arm64-darwin) + tailwindcss-ruby (3.4.19-x86_64-linux) temple (0.10.4) thor (1.5.0) tilt (2.7.0) @@ -340,12 +340,13 @@ GEM zeitwerk (2.8.2) PLATFORMS + aarch64-linux arm64-darwin-25 ruby + x86_64-linux DEPENDENCIES bootsnap - bootstrap (~> 5.3) byebug castle-rb (~> 8.1) devise (~> 5.0) @@ -360,11 +361,11 @@ DEPENDENCIES rails-controller-testing responders rspec-rails - sassc-rails simple_form simplecov sprockets-rails sqlite3 (~> 2.1) + tailwindcss-rails (~> 3.3) web-console RUBY VERSION diff --git a/README.md b/README.md index ec18935..61018a9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,12 @@ SDK (8.x). - **browser SDK** – the `@castleio/castle-js` SDK mints a request token in the browser that is submitted with the login form and forwarded to the API. +## Screenshots + +| Home | Login | +| ---- | ----- | +| ![Home](docs/screenshots/home.png) | ![Login](docs/screenshots/login.png) | + ## Prerequisites You'll need a Castle account. If you don't have one, start a free trial at @@ -55,6 +61,25 @@ bin/rails server `bin/setup` runs the dependency install, file copying and database setup in one step. +## Styling (Tailwind CSS) + +The UI is styled with [Tailwind CSS](https://tailwindcss.com) via the +[`tailwindcss-rails`](https://github.com/rails/tailwindcss-rails) gem (no Node +toolchain required). The source is `app/assets/stylesheets/application.tailwind.css` +with design tokens in `config/tailwind.config.js`; it compiles to +`app/assets/builds/tailwind.css`, which is committed so `bin/rails server` works +without a build step. + +If you change the views or the Tailwind source, regenerate the stylesheet: + +```bash +bin/rails tailwindcss:build # one-off build +bin/rails tailwindcss:watch # rebuild on change during development +``` + +`assets:precompile` (used by the Docker build) runs `tailwindcss:build` +automatically. + ## Configuration All configuration is read from environment variables (loaded from `.env` in diff --git a/app/assets/builds/tailwind.css b/app/assets/builds/tailwind.css new file mode 100644 index 0000000..4c7a143 --- /dev/null +++ b/app/assets/builds/tailwind.css @@ -0,0 +1 @@ +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}body{min-height:100vh;--tw-bg-opacity:1;background-color:rgb(11 14 20/var(--tw-bg-opacity,1));color:rgb(230 233 239/var(--tw-text-opacity,1));font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:15px;line-height:1.625;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-image:radial-gradient(1200px 600px at 80% -10%,rgba(124,92,255,.12),transparent 60%)}a,body{--tw-text-opacity:1}a{color:rgb(124 92 255/var(--tw-text-opacity,1));text-decoration-line:none}a:hover{text-decoration-line:underline}h1,h2,h3,h4{font-weight:600;line-height:1.25}p{margin-bottom:.75rem}code{border-radius:.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(27 34 48/var(--tw-bg-opacity,1));font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.86em;padding:.125rem .375rem}.relative{position:relative}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.table{display:table}.hidden{display:none}.w-full{width:100%}.list-disc{list-style-type:disc}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.border{border-width:1px}.pl-5{padding-left:1.25rem}.text-center{text-align:center}.text-\[1\.2rem\]{font-size:1.2rem}.text-\[1\.3rem\]{font-size:1.3rem}.text-\[1\.4rem\]{font-size:1.4rem}.text-\[2rem\]{font-size:2rem}.text-muted{--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.navbar{border-bottom-width:1px;flex-wrap:wrap;gap:1.5rem;position:sticky;top:0;z-index:50;--tw-border-opacity:1;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);background:rgba(13,16,22,.8);border-color:rgb(35 43 57/var(--tw-border-opacity,1));padding:.875rem 1.5rem}.brand,.navbar{align-items:center;display:flex}.brand{font-size:1.05rem;font-weight:700;gap:.5rem;--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1))}.brand:hover{text-decoration-line:none}.brand-dot{border-radius:9999px;height:.625rem;width:.625rem;--tw-bg-opacity:1;background-color:rgb(124 92 255/var(--tw-bg-opacity,1));box-shadow:0 0 12px #7c5cff}.nav-links{align-items:center;display:flex;flex-wrap:wrap;gap:1.25rem;margin-left:auto}.nav-links a{font-size:.92rem;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.nav-links a:hover{--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1));text-decoration-line:none}.tag{background-color:rgba(124,92,255,.1);border-color:rgba(124,92,255,.4);border-radius:9999px;border-width:1px;font-size:.75rem;font-weight:600;line-height:1rem;padding:.125rem .5rem;--tw-text-opacity:1;color:rgb(124 92 255/var(--tw-text-opacity,1))}.container-page{margin-left:auto;margin-right:auto;max-width:1120px;padding:2rem 1.5rem 4rem}.container-narrow{margin-left:auto;margin-right:auto;max-width:420px;padding:4rem 1.5rem;width:100%}.card{border-radius:14px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(21 26 35/var(--tw-bg-opacity,1));padding:1.5rem;--tw-shadow:0 10px 30px rgba(0,0,0,.35);--tw-shadow-colored:0 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.eyebrow{font-size:.75rem;font-weight:700;letter-spacing:.05em;line-height:1rem;margin-bottom:.375rem;text-transform:uppercase;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.hero{margin-bottom:2rem;padding:2rem}.feature,.hero{border-radius:14px;border-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(21 26 35/var(--tw-bg-opacity,1))}.feature{display:block;padding:1.25rem;text-align:left;transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.feature:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-border-opacity:1;border-color:rgb(124 92 255/var(--tw-border-opacity,1));text-decoration-line:none}.section-head{border-bottom-width:1px;margin-bottom:.75rem;margin-top:2rem;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));padding-bottom:.5rem}.prose-list>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.field{margin-bottom:.875rem}.field label{display:block;font-size:.82rem;font-weight:600;margin-bottom:.375rem;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.input,input[type=email],input[type=password],input[type=text]{border-radius:9px;border-width:1px;width:100%;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(17 21 31/var(--tw-bg-opacity,1));font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:.95rem;padding:.625rem .75rem;--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1));transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.input:focus,input[type=email]:focus,input[type=password]:focus,input[type=text]:focus{--tw-border-opacity:1;border-color:rgb(124 92 255/var(--tw-border-opacity,1));box-shadow:0 0 0 3px rgba(124,92,255,.14);outline:2px solid transparent;outline-offset:2px}.btn{border-radius:9px;border-width:1px;cursor:pointer;display:inline-block;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(27 34 48/var(--tw-bg-opacity,1));font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-size:.92rem;font-weight:600;padding:.625rem 1rem;text-align:center;--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1));text-decoration-line:none;transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn:hover{--tw-border-opacity:1;border-color:rgb(124 92 255/var(--tw-border-opacity,1));text-decoration-line:none}.btn:active{--tw-translate-y:1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.btn-primary{--tw-border-opacity:1;border-color:rgb(124 92 255/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(124 92 255/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.btn-primary:hover{--tw-bg-opacity:1;background-color:rgb(107 76 240/var(--tw-bg-opacity,1))}.btn-alt,.btn-ghost{background-color:transparent}.btn-danger{background-color:rgba(255,92,124,.1);border-color:rgba(255,92,124,.5);--tw-text-opacity:1;color:rgb(255 92 124/var(--tw-text-opacity,1))}.btn-row{display:flex;flex-wrap:wrap;gap:.625rem;margin-top:1rem}.alert{align-items:center;border-radius:9px;border-width:1px;display:flex;font-size:.92rem;gap:1rem;justify-content:space-between;margin-bottom:1rem;padding:.75rem 1rem}.alert-success{background-color:rgba(46,204,113,.1);border-color:rgba(46,204,113,.4)}.alert-danger,.alert-success{--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1))}.alert-danger{background-color:rgba(255,92,124,.1);border-color:rgba(255,92,124,.4)}.alert .btn-close{background-color:transparent;border-width:0;cursor:pointer;font-size:1.125rem;line-height:1.75rem;line-height:1;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.alert .btn-close:hover{--tw-text-opacity:1;color:rgb(230 233 239/var(--tw-text-opacity,1))}table.table{border-collapse:collapse;border-radius:9px;font-size:.9rem;overflow:hidden;width:100%}table.table th{border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(35 43 57/var(--tw-border-opacity,1));--tw-bg-opacity:1;background-color:rgb(27 34 48/var(--tw-bg-opacity,1));font-size:.78rem;font-weight:700;letter-spacing:.025em;padding:.5rem .75rem;text-align:left;text-transform:uppercase;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}table.table td{border-bottom-width:1px;--tw-border-opacity:1;border-color:rgb(28 35 48/var(--tw-border-opacity,1));padding:.5rem .75rem;vertical-align:top}.lead{font-size:1.1rem;--tw-text-opacity:1;color:rgb(154 164 178/var(--tw-text-opacity,1))}.form-actions{margin-top:1.25rem}.error,.invalid-feedback{color:rgb(255 92 124/var(--tw-text-opacity,1))}.error,.hint,.invalid-feedback{display:block;font-size:.8rem;margin-top:.25rem;--tw-text-opacity:1}.hint{color:rgb(154 164 178/var(--tw-text-opacity,1))}.field_with_errors{display:contents} \ No newline at end of file diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index b16e53d..57b152e 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,3 +1,3 @@ //= link_tree ../images //= link_directory ../javascripts .js -//= link_directory ../stylesheets .css +//= link_tree ../builds diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 303dc79..f46e2a9 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -11,6 +11,4 @@ // about supported directives. // //= require rails-ujs -//= require popper -//= require bootstrap //= require_tree . diff --git a/app/assets/javascripts/ui.js b/app/assets/javascripts/ui.js new file mode 100644 index 0000000..74212b4 --- /dev/null +++ b/app/assets/javascripts/ui.js @@ -0,0 +1,20 @@ +// Minimal UI behaviour that previously came from Bootstrap's JS bundle: +// the responsive navbar toggle and dismissible flash messages. +(function () { + document.addEventListener("DOMContentLoaded", function () { + var toggler = document.querySelector("[data-nav-toggle]"); + var menu = document.querySelector("[data-nav-menu]"); + if (toggler && menu) { + toggler.addEventListener("click", function () { + menu.classList.toggle("hidden"); + }); + } + + document.querySelectorAll("[data-dismiss-alert]").forEach(function (btn) { + btn.addEventListener("click", function () { + var alert = btn.closest(".alert"); + if (alert) alert.remove(); + }); + }); + }); +})(); diff --git a/app/assets/stylesheets/1st_load_framework.css.scss b/app/assets/stylesheets/1st_load_framework.css.scss deleted file mode 100644 index 426d705..0000000 --- a/app/assets/stylesheets/1st_load_framework.css.scss +++ /dev/null @@ -1,40 +0,0 @@ -@import 'bootstrap'; - -img { - @extend .img-fluid; - margin: 0 auto; -} - -.navbar-brand { - font-size: inherit; -} - -.column { - @extend .col-md-6; - text-align: center; -} - -.form { - @extend .col-md-6; -} - -.form-centered { - @extend .col-md-6; - text-align: center; -} - -.submit { - @extend .btn; - @extend .btn-primary; - @extend .btn-lg; -} - -main { - @extend .container; - margin-top: 30px; -} - -section { - @extend .row; - margin-top: 20px; -} diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss deleted file mode 100644 index d05ea0f..0000000 --- a/app/assets/stylesheets/application.css.scss +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css new file mode 100644 index 0000000..cc3b33a --- /dev/null +++ b/app/assets/stylesheets/application.tailwind.css @@ -0,0 +1,194 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply min-h-screen bg-bg font-sans text-[15px] leading-relaxed text-ink antialiased; + background-image: radial-gradient( + 1200px 600px at 80% -10%, + rgba(124, 92, 255, 0.12), + transparent 60% + ); + } + + a { + @apply text-accent no-underline hover:underline; + } + + h1, + h2, + h3, + h4 { + @apply font-semibold leading-tight; + } + + p { + @apply mb-3; + } + + code { + @apply rounded border border-border bg-surface-2 px-1.5 py-0.5 font-mono text-[0.86em]; + } +} + +/* + * Shared design system component classes (kept in sync with the Node and Python + * Castle example apps). Authored outside @layer so they are always emitted. + */ + +.navbar { + @apply sticky top-0 z-50 flex flex-wrap items-center gap-6 border-b border-border px-6 py-3.5; + background: rgba(13, 16, 22, 0.8); + backdrop-filter: blur(10px); +} + +.brand { + @apply flex items-center gap-2 text-[1.05rem] font-bold text-ink hover:no-underline; +} + +.brand-dot { + @apply h-2.5 w-2.5 rounded-full bg-accent; + box-shadow: 0 0 12px #7c5cff; +} + +.nav-links { + @apply ml-auto flex flex-wrap items-center gap-5; +} + +.nav-links a { + @apply text-[0.92rem] text-muted hover:text-ink hover:no-underline; +} + +.tag { + @apply rounded-full border border-accent/40 bg-accent/10 px-2 py-0.5 text-xs font-semibold text-accent; +} + +.container-page { + @apply mx-auto max-w-[1120px] px-6 pb-16 pt-8; +} + +.container-narrow { + @apply mx-auto w-full max-w-[420px] px-6 py-16; +} + +.card { + @apply rounded-xl border border-border bg-surface p-6 shadow-card; +} + +.eyebrow { + @apply mb-1.5 text-xs font-bold uppercase tracking-wider text-muted; +} + +.hero { + @apply mb-8 rounded-xl border border-border bg-surface p-8; +} + +.feature { + @apply block rounded-xl border border-border bg-surface p-5 text-left transition hover:-translate-y-0.5 hover:border-accent hover:no-underline; +} + +.section-head { + @apply mb-3 mt-8 border-b border-border pb-2; +} + +.prose-list { + @apply space-y-2; +} + +.field { + @apply mb-3.5; +} + +.field label { + @apply mb-1.5 block text-[0.82rem] font-semibold text-muted; +} + +.input, +input[type='text'], +input[type='email'], +input[type='password'] { + @apply w-full rounded-lg border border-border bg-bg-soft px-3 py-2.5 font-sans text-[0.95rem] text-ink transition; +} + +.input:focus, +input[type='text']:focus, +input[type='email']:focus, +input[type='password']:focus { + @apply border-accent outline-none; + box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.14); +} + +.btn { + @apply inline-block cursor-pointer rounded-lg border border-border bg-surface-2 px-4 py-2.5 text-center font-sans text-[0.92rem] font-semibold text-ink no-underline transition hover:border-accent hover:no-underline active:translate-y-px; +} + +.btn-primary { + @apply border-accent bg-accent text-white hover:bg-accent-hover; +} + +.btn-ghost, +.btn-alt { + @apply bg-transparent; +} + +.btn-danger { + @apply border-danger/50 bg-danger/10 text-danger; +} + +.btn-row { + @apply mt-4 flex flex-wrap gap-2.5; +} + +/* Flash messages (replacing Bootstrap alerts). */ +.alert { + @apply mb-4 flex items-center justify-between gap-4 rounded-lg border px-4 py-3 text-[0.92rem]; +} + +.alert-success { + @apply border-success/40 bg-success/10 text-ink; +} + +.alert-danger { + @apply border-danger/40 bg-danger/10 text-ink; +} + +.alert .btn-close { + @apply cursor-pointer border-0 bg-transparent text-lg leading-none text-muted hover:text-ink; +} + +/* Tables (webhooks list). */ +table.table { + @apply w-full border-collapse overflow-hidden rounded-lg text-[0.9rem]; +} + +table.table th { + @apply border-b border-border bg-surface-2 px-3 py-2 text-left text-[0.78rem] font-bold uppercase tracking-wide text-muted; +} + +table.table td { + @apply border-b border-border-soft px-3 py-2 align-top; +} + +.lead { + @apply text-[1.1rem] text-muted; +} + +.form-actions { + @apply mt-5; +} + +/* simple_form error/hint output. */ +.error, +.invalid-feedback { + @apply mt-1 block text-[0.8rem] text-danger; +} + +.hint { + @apply mt-1 block text-[0.8rem] text-muted; +} + +/* Rails wraps invalid fields in this div; keep it from breaking layout. */ +.field_with_errors { + display: contents; +} diff --git a/app/assets/stylesheets/dashboard.css.scss b/app/assets/stylesheets/dashboard.css.scss deleted file mode 100644 index d20ceae..0000000 --- a/app/assets/stylesheets/dashboard.css.scss +++ /dev/null @@ -1,93 +0,0 @@ -body { - background-color: #f5f5f5; - font-size: .875rem; -} - -.pull-right { - float: right; -} - -#logo { - img { height: 25px; } -} - -.feather { - height: 16px; - vertical-align: text-bottom; - width: 16px; -} - -.sidebar { - position: fixed; - top: 0; - bottom: 0; - left: 0; - z-index: 100; /* Behind the navbar */ - padding: 48px 0 0; /* Height of navbar */ - box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); -} - -.sidebar-sticky { - position: relative; - top: 0; - height: calc(100vh - 48px); - padding-top: .5rem; - overflow-x: hidden; - overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ -} - -@supports ((position: -webkit-sticky) or (position: sticky)) { - .sidebar-sticky { - position: -webkit-sticky; - position: sticky; - } -} - -.sidebar .nav-link { - font-weight: 500; - color: #333; -} - -.sidebar .nav-link .feather { - margin-right: 4px; - color: #999; -} - -.sidebar .nav-link.active { - color: #007bff; -} - -.sidebar .nav-link:hover .feather, -.sidebar .nav-link.active .feather { - color: inherit; -} - -.sidebar-heading { - font-size: .75rem; - text-transform: uppercase; -} - -[role="main"] { - padding-top: 5rem; /* Space for fixed navbar */ -} - -.navbar .form-control { - padding: .75rem 1rem; - border-width: 0; - border-radius: 0; -} - -.form-control-dark { - color: #fff; - background-color: rgba(255, 255, 255, .1); - border-color: rgba(255, 255, 255, .1); -} - -.form-control-dark:focus { - border-color: transparent; - box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); -} - - -.border-top { border-top: 1px solid #e5e5e5; } -.border-bottom { border-bottom: 1px solid #e5e5e5; } diff --git a/app/assets/stylesheets/signin.css.scss b/app/assets/stylesheets/signin.css.scss deleted file mode 100644 index 08d335f..0000000 --- a/app/assets/stylesheets/signin.css.scss +++ /dev/null @@ -1,40 +0,0 @@ -body.signin { - height: 100%; - display: -ms-flexbox; - display: flex; - -ms-flex-align: center; - align-items: center; - padding-top: 40px; - padding-bottom: 40px; - background-color: #f5f5f5; - - .form-signin { - width: 100%; - max-width: 330px; - padding: 15px; - margin: auto; - } - .form-signin .checkbox { - font-weight: 400; - } - .form-signin .form-control { - position: relative; - box-sizing: border-box; - height: auto; - padding: 10px; - font-size: 16px; - } - .form-signin .form-control:focus { - z-index: 2; - } - .form-signin input[type="email"] { - margin-bottom: -1px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - } - .form-signin input[type="password"] { - margin-bottom: 10px; - border-top-left-radius: 0; - border-top-right-radius: 0; - } -} diff --git a/app/views/integrations/castle_webhooks/index.html.haml b/app/views/integrations/castle_webhooks/index.html.haml index f2509d1..136db7e 100644 --- a/app/views/integrations/castle_webhooks/index.html.haml +++ b/app/views/integrations/castle_webhooks/index.html.haml @@ -1,13 +1,12 @@ -.d-flex.justify-content-between.flex-wrap.align-items-center.pt-3.pb-2.mb-3.border-bottom - %h2.h2 Received Castle webhooks +.section-head + %h2{ class: 'text-[1.4rem]' } Received Castle webhooks %p.lead This page lists 50 most recent webhooks received from Castle. -%p - %table.table - %tr - %th Webhook local ID - %th Webhook body - %th Created at - = render @castle_webhooks +%table.table.mt-4 + %tr + %th Webhook local ID + %th Webhook body + %th Created at + = render @castle_webhooks diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index c9b4cb0..845bca8 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -4,6 +4,10 @@ %title= t('shared.title') - = stylesheet_link_tag 'application', media: 'all' + %link{ rel: 'preconnect', href: 'https://fonts.googleapis.com' } + %link{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: true } + %link{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap' } + + = stylesheet_link_tag 'tailwind', media: 'all' = javascript_include_tag 'application' = csrf_meta_tags diff --git a/app/views/layouts/_messages.html.haml b/app/views/layouts/_messages.html.haml index 066557b..071a409 100644 --- a/app/views/layouts/_messages.html.haml +++ b/app/views/layouts/_messages.html.haml @@ -1,5 +1,6 @@ - flash.each do |name, msg| - if msg.is_a?(String) - %div{ class: "alert alert-#{name.to_s == 'notice' ? 'success' : 'danger'} alert-dismissible fade show", role: 'alert' } + %div{ class: "alert alert-#{name.to_s == 'notice' ? 'success' : 'danger'}", role: 'alert' } = content_tag :span, msg, id: "flash_#{name}" - %button.btn-close{ type: 'button', 'data-bs-dismiss' => 'alert', 'aria-label' => 'Close' } + %button.btn-close{ type: 'button', 'data-dismiss-alert' => true, 'aria-label' => 'Close' } + × diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f825484..109d6c4 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -3,34 +3,23 @@ = render 'layouts/header' %body - %nav.navbar.navbar-expand-lg.fixed-top.bg-dark.shadow-sm{ 'data-bs-theme' => 'dark' } - .container-fluid - = link_to image_tag('logo.png', height: 28, alt: 'Castle'), root_path, class: 'navbar-brand', id: 'logo' + %nav.navbar + = link_to root_path, class: 'brand', id: 'logo' do + %span.brand-dot + Castle + %span.text-muted{ style: 'font-weight:400' } demo + .nav-links + = link_to t('.nav.webhooks'), integrations_castle_webhooks_path - %button.navbar-toggler{ type: 'button', 'data-bs-toggle' => 'collapse', - 'data-bs-target' => '#navbar-nav', 'aria-controls' => 'navbar-nav', - 'aria-expanded' => 'false', 'aria-label' => 'Toggle navigation' } - %span.navbar-toggler-icon + - if user_signed_in? + = link_to t('.nav.edit_password'), edit_user_registration_path + = link_to t('.nav.edit_profile'), edit_users_profile_path + = link_to t('.nav.sign_out'), destroy_user_session_path, method: :delete + - else + = link_to t('.nav.login'), new_user_session_path + = link_to t('.nav.register'), new_user_registration_path - #navbar-nav.collapse.navbar-collapse - %ul.navbar-nav.ms-auto - %li.nav-item - = link_to t('.nav.webhooks'), integrations_castle_webhooks_path, class: 'nav-link' - - - if user_signed_in? - %li.nav-item - = link_to t('.nav.edit_password'), edit_user_registration_path, class: 'nav-link' - %li.nav-item - = link_to t('.nav.edit_profile'), edit_users_profile_path, class: 'nav-link' - %li.nav-item - = link_to t('.nav.sign_out'), destroy_user_session_path, method: :delete, class: 'nav-link' - - else - %li.nav-item - = link_to t('.nav.login'), new_user_session_path, class: 'nav-link' - %li.nav-item - = link_to t('.nav.register'), new_user_registration_path, class: 'nav-link' - - %main.container{ role: 'main' } + %main.container-page{ role: 'main' } = render 'layouts/messages' = yield diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index e9687ed..95af10c 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,15 +1,18 @@ !!! -%html +%html{ lang: 'en' } = render 'layouts/header' - %body.signin - .form-signin - %p.text-center - %a{ href: root_path } - = image_tag 'logo_plain.png', alt: '', width: 72, class: 'mb-4' + %body + .container-narrow + %p.text-center.mb-6 + = link_to root_path, class: 'brand justify-center text-[1.3rem]' do + %span.brand-dot + Castle + %span.text-muted{ style: 'font-weight:400' } demo = render 'layouts/messages' - = yield + .card + = yield = render 'layouts/castle_js' diff --git a/app/views/main/index.html.haml b/app/views/main/index.html.haml index 2834074..ab995d4 100644 --- a/app/views/main/index.html.haml +++ b/app/views/main/index.html.haml @@ -1,16 +1,16 @@ -.p-5.mb-4.bg-body-tertiary.rounded-3 - .container-fluid.py-3 - %h1.display-5.fw-bold Castle Rails example app - %p.col-md-10.fs-5 - A minimal Ruby on Rails + Devise application showing how to wire the - = succeed '' do - %a{ href: 'https://github.com/castle/castle-ruby' } castle-rb - SDK into a real authentication flow. +.hero + %span.tag castle-rb 8.1 · rails + %h1{ class: 'text-[2rem] mt-3' } Castle Rails example app + %p.lead.mt-2 + A minimal Ruby on Rails + Devise application showing how to wire the + = succeed '' do + %a{ href: 'https://github.com/castle/castle-ruby' } castle-rb + SDK into a real authentication flow. -.d-flex.justify-content-between.flex-wrap.align-items-center.pt-3.pb-2.mb-3.border-bottom - %h2.h3 What's demonstrated +.section-head + %h2{ class: 'text-[1.2rem]' } What's demonstrated -%ul +%ul.prose-list.list-disc.pl-5 %li %strong Login — successful logins are scored with the @@ -31,14 +31,14 @@ — incoming Castle webhooks are signature-verified and listed under = link_to 'Webhooks', integrations_castle_webhooks_path -.d-flex.justify-content-between.flex-wrap.align-items-center.pt-3.pb-2.mb-3.border-bottom - %h2.h3 Configuration +.section-head + %h2{ class: 'text-[1.2rem]' } Configuration %p Copy - %code.env.example + %code .env.example to - %code.env + %code .env and fill in your Castle credentials (CASTLE_API_SECRET and CASTLE_PK) from the = succeed '.' do diff --git a/app/views/users/profiles/edit.html.haml b/app/views/users/profiles/edit.html.haml index f5454f2..6e96bd3 100644 --- a/app/views/users/profiles/edit.html.haml +++ b/app/views/users/profiles/edit.html.haml @@ -5,7 +5,7 @@ .form-inputs = f.input :email, required: true, autofocus: true .form-actions - = f.button :submit, t('.button'), class: 'btn btn-lg btn-primary btn-block' + = f.button :submit, t('.button'), class: 'btn btn-primary w-full' -%p.mb-4.mt-4 - = link_to t('.back'), root_path, class: 'btn btn-secondary' +.btn-row + = link_to t('.back'), root_path, class: 'btn' diff --git a/app/views/users/registrations/edit.html.haml b/app/views/users/registrations/edit.html.haml index 75a9105..ca9c655 100644 --- a/app/views/users/registrations/edit.html.haml +++ b/app/views/users/registrations/edit.html.haml @@ -11,12 +11,12 @@ = f.input :password_confirmation, required: false = f.input :current_password, hint: t('.current_password_hint'), required: true .form-actions - = f.button :submit, t('.button'), class: 'btn btn-lg btn-primary btn-block' + = f.button :submit, t('.button'), class: 'btn btn-primary w-full' -%p.mb-4.mt-4 - = link_to t('.back'), root_path, class: 'btn btn-secondary' +.btn-row.justify-between + = link_to t('.back'), root_path, class: 'btn' = link_to t('.cancel'), registration_path(resource_name), data: { confirm: t('shared.confirm') }, method: :delete, - class: 'btn btn-danger pull-right' + class: 'btn btn-danger' diff --git a/app/views/users/registrations/new.html.haml b/app/views/users/registrations/new.html.haml index 1fb89e4..4fc0f5e 100644 --- a/app/views/users/registrations/new.html.haml +++ b/app/views/users/registrations/new.html.haml @@ -8,6 +8,6 @@ = f.input :password, required: true, hint: hint = f.input :password_confirmation, required: true .form-actions - = f.button :submit, t('.button'), class: 'btn btn-lg btn-primary btn-block' + = f.button :submit, t('.button'), class: 'btn btn-primary w-full' = render "users/shared/links" diff --git a/app/views/users/sessions/new.html.haml b/app/views/users/sessions/new.html.haml index 1eb0a0a..418ac5c 100644 --- a/app/views/users/sessions/new.html.haml +++ b/app/views/users/sessions/new.html.haml @@ -5,6 +5,6 @@ = f.input :email, required: false, autofocus: true = f.input :password, required: false .form-actions - = f.button :submit, t('.button'), class: 'btn btn-lg btn-primary btn-block' + = f.button :submit, t('.button'), class: 'btn btn-primary w-full' = render 'users/shared/links' diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index d268784..9a8de87 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -13,7 +13,7 @@ # wrapper, change the order or even add your own to the # stack. The options given below are used to wrap the # whole input. - config.wrappers :default, class: :input, + config.wrappers :default, class: 'field', hint_class: :field_with_hint, error_class: :field_with_errors, valid_class: :field_without_errors do |b| ## Extensions enabled by default # Any of these extensions can be disabled for a @@ -74,7 +74,7 @@ config.boolean_style = :nested # Default class for buttons - config.button_class = 'btn' + config.button_class = 'btn btn-primary' # Method used to tidy up errors. Specify any Rails Array method. # :first lists the first message for each field. @@ -85,7 +85,7 @@ config.error_notification_tag = :div # CSS class to add for error notification helper. - config.error_notification_class = 'error_notification' + config.error_notification_class = 'alert alert-danger' # Series of attempts to detect a default label method for collection. # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] @@ -158,7 +158,7 @@ # config.cache_discovery = !Rails.env.development? # Default class for inputs - # config.input_class = nil + config.input_class = 'input' # Define the default class of the input wrapper of the boolean input. config.boolean_label_class = 'checkbox' diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb deleted file mode 100644 index 7ec2ec6..0000000 --- a/config/initializers/simple_form_bootstrap.rb +++ /dev/null @@ -1,372 +0,0 @@ -# frozen_string_literal: true - -# These defaults are defined and maintained by the community at -# https://github.com/heartcombo/simple_form-bootstrap -# Please submit feedback, changes and tests only there. - -# Uncomment this and change the path if necessary to include your own -# components. -# See https://github.com/heartcombo/simple_form#custom-components -# to know more about custom components. -# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } - -# Use this setup block to configure all options available in SimpleForm. -SimpleForm.setup do |config| - # Default class for buttons - config.button_class = 'btn' - - # Define the default class of the input wrapper of the boolean input. - config.boolean_label_class = 'form-check-label' - - # How the label text should be generated altogether with the required text. - config.label_text = lambda { |label, required, explicit_label| "#{label} #{required}" } - - # Define the way to render check boxes / radio buttons with labels. - config.boolean_style = :inline - - # You can wrap each item in a collection of radio/check boxes with a tag - config.item_wrapper_tag = :div - - # Defines if the default input wrapper class should be included in radio - # collection wrappers. - config.include_default_input_wrapper_class = false - - # CSS class to add for error notification helper. - config.error_notification_class = 'alert alert-danger' - - # Method used to tidy up errors. Specify any Rails Array method. - # :first lists the first message for each field. - # :to_sentence to list all errors for each field. - config.error_method = :to_sentence - - # add validation classes to `input_field` - config.input_field_error_class = 'is-invalid' - config.input_field_valid_class = 'is-valid' - - - # vertical forms - # - # vertical default_wrapper - config.wrappers :vertical_form, class: 'mb-3' do |b| - b.use :html5 - b.use :placeholder - b.optional :maxlength - b.optional :minlength - b.optional :pattern - b.optional :min_max - b.optional :readonly - b.use :label, class: 'form-label' - b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :full_error, wrap_with: { class: 'invalid-feedback' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - # vertical input for boolean - config.wrappers :vertical_boolean, tag: 'fieldset', class: 'mb-3' do |b| - b.use :html5 - b.optional :readonly - b.wrapper :form_check_wrapper, class: 'form-check' do |bb| - bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' - bb.use :label, class: 'form-check-label' - bb.use :full_error, wrap_with: { class: 'invalid-feedback' } - bb.use :hint, wrap_with: { class: 'form-text' } - end - end - - # vertical input for radio buttons and check boxes - config.wrappers :vertical_collection, item_wrapper_class: 'form-check', item_label_class: 'form-check-label', tag: 'fieldset', class: 'mb-3' do |b| - b.use :html5 - b.optional :readonly - b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| - ba.use :label_text - end - b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - # vertical input for inline radio buttons and check boxes - config.wrappers :vertical_collection_inline, item_wrapper_class: 'form-check form-check-inline', item_label_class: 'form-check-label', tag: 'fieldset', class: 'mb-3' do |b| - b.use :html5 - b.optional :readonly - b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| - ba.use :label_text - end - b.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - # vertical file input - config.wrappers :vertical_file, class: 'mb-3' do |b| - b.use :html5 - b.use :placeholder - b.optional :maxlength - b.optional :minlength - b.optional :readonly - b.use :label, class: 'form-label' - b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :full_error, wrap_with: { class: 'invalid-feedback' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - # vertical select input - config.wrappers :vertical_select, class: 'mb-3' do |b| - b.use :html5 - b.optional :readonly - b.use :label, class: 'form-label' - b.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :full_error, wrap_with: { class: 'invalid-feedback' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - # vertical multi select - config.wrappers :vertical_multi_select, class: 'mb-3' do |b| - b.use :html5 - b.optional :readonly - b.use :label, class: 'form-label' - b.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |ba| - ba.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' - end - b.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - # vertical range input - config.wrappers :vertical_range, class: 'mb-3' do |b| - b.use :html5 - b.use :placeholder - b.optional :readonly - b.optional :step - b.use :label, class: 'form-label' - b.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :full_error, wrap_with: { class: 'invalid-feedback' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - - # horizontal forms - # - # horizontal default_wrapper - config.wrappers :horizontal_form, class: 'row mb-3' do |b| - b.use :html5 - b.use :placeholder - b.optional :maxlength - b.optional :minlength - b.optional :pattern - b.optional :min_max - b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| - ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' - ba.use :full_error, wrap_with: { class: 'invalid-feedback' } - ba.use :hint, wrap_with: { class: 'form-text' } - end - end - - # horizontal input for boolean - config.wrappers :horizontal_boolean, class: 'row mb-3' do |b| - b.use :html5 - b.optional :readonly - b.wrapper :grid_wrapper, class: 'col-sm-9 offset-sm-3' do |wr| - wr.wrapper :form_check_wrapper, class: 'form-check' do |bb| - bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' - bb.use :label, class: 'form-check-label' - bb.use :full_error, wrap_with: { class: 'invalid-feedback' } - bb.use :hint, wrap_with: { class: 'form-text' } - end - end - end - - # horizontal input for radio buttons and check boxes - config.wrappers :horizontal_collection, item_wrapper_class: 'form-check', item_label_class: 'form-check-label', class: 'row mb-3' do |b| - b.use :html5 - b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label pt-0' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| - ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' - ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } - ba.use :hint, wrap_with: { class: 'form-text' } - end - end - - # horizontal input for inline radio buttons and check boxes - config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline', item_label_class: 'form-check-label', class: 'row mb-3' do |b| - b.use :html5 - b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label pt-0' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| - ba.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' - ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } - ba.use :hint, wrap_with: { class: 'form-text' } - end - end - - # horizontal file input - config.wrappers :horizontal_file, class: 'row mb-3' do |b| - b.use :html5 - b.use :placeholder - b.optional :maxlength - b.optional :minlength - b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| - ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' - ba.use :full_error, wrap_with: { class: 'invalid-feedback' } - ba.use :hint, wrap_with: { class: 'form-text' } - end - end - - # horizontal select input - config.wrappers :horizontal_select, class: 'row mb-3' do |b| - b.use :html5 - b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| - ba.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' - ba.use :full_error, wrap_with: { class: 'invalid-feedback' } - ba.use :hint, wrap_with: { class: 'form-text' } - end - end - - # horizontal multi select - config.wrappers :horizontal_multi_select, class: 'row mb-3' do |b| - b.use :html5 - b.optional :readonly - b.use :label, class: 'col-sm-3 col-form-label' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| - ba.wrapper class: 'd-flex flex-row justify-content-between align-items-center' do |bb| - bb.use :input, class: 'form-select mx-1', error_class: 'is-invalid', valid_class: 'is-valid' - end - ba.use :full_error, wrap_with: { class: 'invalid-feedback d-block' } - ba.use :hint, wrap_with: { class: 'form-text' } - end - end - - # horizontal range input - config.wrappers :horizontal_range, class: 'row mb-3' do |b| - b.use :html5 - b.use :placeholder - b.optional :readonly - b.optional :step - b.use :label, class: 'col-sm-3 col-form-label pt-0' - b.wrapper :grid_wrapper, class: 'col-sm-9' do |ba| - ba.use :input, class: 'form-range', error_class: 'is-invalid', valid_class: 'is-valid' - ba.use :full_error, wrap_with: { class: 'invalid-feedback' } - ba.use :hint, wrap_with: { class: 'form-text' } - end - end - - - # inline forms - # - # inline default_wrapper - config.wrappers :inline_form, class: 'col-12' do |b| - b.use :html5 - b.use :placeholder - b.optional :maxlength - b.optional :minlength - b.optional :pattern - b.optional :min_max - b.optional :readonly - b.use :label, class: 'visually-hidden' - - b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :error, wrap_with: { class: 'invalid-feedback' } - b.optional :hint, wrap_with: { class: 'form-text' } - end - - # inline input for boolean - config.wrappers :inline_boolean, class: 'col-12' do |b| - b.use :html5 - b.optional :readonly - b.wrapper :form_check_wrapper, class: 'form-check' do |bb| - bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' - bb.use :label, class: 'form-check-label' - bb.use :error, wrap_with: { class: 'invalid-feedback' } - bb.optional :hint, wrap_with: { class: 'form-text' } - end - end - - - # bootstrap custom forms - # - # custom input switch for boolean - config.wrappers :custom_boolean_switch, class: 'mb-3' do |b| - b.use :html5 - b.optional :readonly - b.wrapper :form_check_wrapper, tag: 'div', class: 'form-check form-switch' do |bb| - bb.use :input, class: 'form-check-input', error_class: 'is-invalid', valid_class: 'is-valid' - bb.use :label, class: 'form-check-label' - bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' } - bb.use :hint, wrap_with: { class: 'form-text' } - end - end - - - # Input Group - custom component - # see example app and config at https://github.com/heartcombo/simple_form-bootstrap - config.wrappers :input_group, class: 'mb-3' do |b| - b.use :html5 - b.use :placeholder - b.optional :maxlength - b.optional :minlength - b.optional :pattern - b.optional :min_max - b.optional :readonly - b.use :label, class: 'form-label' - b.wrapper :input_group_tag, class: 'input-group' do |ba| - ba.optional :prepend - ba.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' - ba.optional :append - ba.use :full_error, wrap_with: { class: 'invalid-feedback' } - end - b.use :hint, wrap_with: { class: 'form-text' } - end - - - # Floating Labels form - # - # floating labels default_wrapper - config.wrappers :floating_labels_form, class: 'form-floating mb-3' do |b| - b.use :html5 - b.use :placeholder - b.optional :maxlength - b.optional :minlength - b.optional :pattern - b.optional :min_max - b.optional :readonly - b.use :input, class: 'form-control', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :label - b.use :full_error, wrap_with: { class: 'invalid-feedback' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - # custom multi select - config.wrappers :floating_labels_select, class: 'form-floating mb-3' do |b| - b.use :html5 - b.optional :readonly - b.use :input, class: 'form-select', error_class: 'is-invalid', valid_class: 'is-valid' - b.use :label - b.use :full_error, wrap_with: { class: 'invalid-feedback' } - b.use :hint, wrap_with: { class: 'form-text' } - end - - - # The default wrapper to be used by the FormBuilder. - config.default_wrapper = :vertical_form - - # Custom wrappers for input types. This should be a hash containing an input - # type as key and the wrapper that will be used for all inputs with specified type. - config.wrapper_mappings = { - boolean: :vertical_boolean, - check_boxes: :vertical_collection, - date: :vertical_multi_select, - datetime: :vertical_multi_select, - file: :vertical_file, - radio_buttons: :vertical_collection, - range: :vertical_range, - time: :vertical_multi_select, - select: :vertical_select - } -end diff --git a/config/tailwind.config.js b/config/tailwind.config.js new file mode 100644 index 0000000..e259ff5 --- /dev/null +++ b/config/tailwind.config.js @@ -0,0 +1,41 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './public/*.html', + './app/helpers/**/*.rb', + './app/views/**/*.{erb,haml,html,slim}', + './app/assets/javascripts/**/*.js', + './config/initializers/simple_form*.rb', + ], + theme: { + extend: { + colors: { + bg: '#0b0e14', + 'bg-soft': '#11151f', + surface: '#151a23', + 'surface-2': '#1b2230', + border: '#232b39', + 'border-soft': '#1c2330', + ink: '#e6e9ef', + muted: '#9aa4b2', + accent: '#7c5cff', + 'accent-hover': '#6b4cf0', + success: '#2ecc71', + challenge: '#ffbf47', + danger: '#ff5c7c', + }, + fontFamily: { + sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], + mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'], + }, + borderRadius: { + xl: '14px', + lg: '9px', + }, + boxShadow: { + card: '0 10px 30px rgba(0, 0, 0, 0.35)', + }, + }, + }, + plugins: [], +}; diff --git a/docs/screenshots/home.png b/docs/screenshots/home.png new file mode 100644 index 0000000..33126a7 Binary files /dev/null and b/docs/screenshots/home.png differ diff --git a/docs/screenshots/login.png b/docs/screenshots/login.png new file mode 100644 index 0000000..a2498c5 Binary files /dev/null and b/docs/screenshots/login.png differ