tina4ruby 3.13.81 → 3.13.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -2
- data/README.md +4 -4
- data/lib/tina4/background.rb +21 -2
- data/lib/tina4/cli.rb +141 -14
- data/lib/tina4/mqtt.rb +800 -0
- data/lib/tina4/mqtt_message.rb +78 -0
- data/lib/tina4/public/css/tina4.css +56 -130
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/rack_app.rb +26 -0
- data/lib/tina4/router.rb +13 -2
- data/lib/tina4/scss/tina4css/_pagination.scss +63 -0
- data/lib/tina4/scss/tina4css/_utilities.scss +11 -0
- data/lib/tina4/scss/tina4css/tina4.scss +1 -0
- data/lib/tina4/scss_compiler.rb +48 -3
- data/lib/tina4/test_client.rb +39 -52
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +41 -3
- metadata +34 -3
- /data/lib/tina4/{sql_translation.rb → sql_translator.rb} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
*,*::before,*::after{box-sizing:border-box}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;scroll-behavior:smooth}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem;font-weight:500;line-height:1.25}p{margin-top:0;margin-bottom:1rem}ol,ul{padding-left:2rem;margin-top:0;margin-bottom:1rem}a{color:#4a90d9;text-decoration:none}a:hover{color:#256ab1;text-decoration:underline}img,svg{max-width:100%;height:auto;vertical-align:middle}table{border-collapse:collapse;caption-side:bottom}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button;cursor:pointer}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid rgba(0,0,0,0.1);opacity:0.25}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}[hidden]{display:none !important}h1{font-size:2.25rem}h2{font-size:1.875rem}h3{font-size:1.5rem}h4{font-size:1.25rem}h5{font-size:1.125rem}h6{font-size:1rem}small,.small{font-size:0.875em}mark,.mark{padding:0.2em;background-color:rgba(255,193,7,0.3)}blockquote{margin:0 0 1rem;padding:0.5rem 1rem;border-left:0.25rem solid rgba(0,0,0,0.1);font-size:1.125rem}code{font-size:0.875em;color:#dc3545;word-wrap:break-word}pre{display:block;padding:1rem;font-size:0.875em;color:#212529;background-color:#f8f9fa;border-radius:.25rem}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:0.5rem}.container,.container-fluid{width:100%;padding-right:calc($grid-gutter / 2);padding-left:calc($grid-gutter / 2);margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}@media (min-width: 1400px){.container{max-width:1320px}}.row{display:flex;flex-wrap:wrap;margin-right:calc($grid-gutter / -2);margin-left:calc($grid-gutter / -2)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc($grid-gutter / 2);padding-left:calc($grid-gutter / 2)}.col{flex:1 0 0%}.col-1{flex:0 0 auto;width:8.33333%}.col-2{flex:0 0 auto;width:16.66667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333%}.col-5{flex:0 0 auto;width:41.66667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333%}.col-8{flex:0 0 auto;width:66.66667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333%}.col-11{flex:0 0 auto;width:91.66667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-0{margin-left:0}@media (min-width: 576px){.col-sm{flex:1 0 0%}.col-sm-1{flex:0 0 auto;width:8.33333%}.col-sm-2{flex:0 0 auto;width:16.66667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333%}.col-sm-5{flex:0 0 auto;width:41.66667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333%}.col-sm-8{flex:0 0 auto;width:66.66667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333%}.col-sm-11{flex:0 0 auto;width:91.66667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}}@media (min-width: 768px){.col-md{flex:1 0 0%}.col-md-1{flex:0 0 auto;width:8.33333%}.col-md-2{flex:0 0 auto;width:16.66667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333%}.col-md-5{flex:0 0 auto;width:41.66667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333%}.col-md-8{flex:0 0 auto;width:66.66667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333%}.col-md-11{flex:0 0 auto;width:91.66667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}}@media (min-width: 992px){.col-lg{flex:1 0 0%}.col-lg-1{flex:0 0 auto;width:8.33333%}.col-lg-2{flex:0 0 auto;width:16.66667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333%}.col-lg-5{flex:0 0 auto;width:41.66667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333%}.col-lg-8{flex:0 0 auto;width:66.66667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333%}.col-lg-11{flex:0 0 auto;width:91.66667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}}@media (min-width: 1200px){.col-xl{flex:1 0 0%}.col-xl-1{flex:0 0 auto;width:8.33333%}.col-xl-2{flex:0 0 auto;width:16.66667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333%}.col-xl-5{flex:0 0 auto;width:41.66667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333%}.col-xl-8{flex:0 0 auto;width:66.66667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333%}.col-xl-11{flex:0 0 auto;width:91.66667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.align-start{align-items:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;user-select:none;background-color:transparent;border:1px solid transparent;padding:0.375rem 0.75rem;font-size:1rem;border-radius:.25rem;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;text-decoration:none}.btn:disabled,.btn.disabled{pointer-events:none;opacity:0.65}.btn-primary{color:#fff;background-color:#4a90d9;border-color:#4a90d9}.btn-primary:hover{background-color:#2b7bcf;border-color:#2a76c6}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{background-color:#596167;border-color:#545b62}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{background-color:#208637;border-color:#1e7e34}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{background-color:#c62232;border-color:#bd2130}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{background-color:#dda600;border-color:#d39e00}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{background-color:#128294;border-color:#117a8b}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{background-color:#e0e5e9;border-color:#dae0e5}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{background-color:#0f1112;border-color:#0a0c0d}.btn-outline-primary{color:#4a90d9;border-color:#4a90d9;background-color:transparent}.btn-outline-primary:hover{color:#fff;background-color:#4a90d9}.btn-outline-secondary{color:#6c757d;border-color:#6c757d;background-color:transparent}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d}.btn-outline-success{color:#28a745;border-color:#28a745;background-color:transparent}.btn-outline-success:hover{color:#fff;background-color:#28a745}.btn-outline-danger{color:#dc3545;border-color:#dc3545;background-color:transparent}.btn-outline-danger:hover{color:#fff;background-color:#dc3545}.btn-outline-warning{color:#ffc107;border-color:#ffc107;background-color:transparent}.btn-outline-warning:hover{color:#212529;background-color:#ffc107}.btn-outline-info{color:#17a2b8;border-color:#17a2b8;background-color:transparent}.btn-outline-info:hover{color:#fff;background-color:#17a2b8}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa;background-color:transparent}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa}.btn-outline-dark{color:#212529;border-color:#212529;background-color:transparent}.btn-outline-dark:hover{color:#fff;background-color:#212529}.btn-sm{padding:0.25rem 0.5rem;font-size:.875rem;border-radius:.25rem}.btn-lg{padding:0.5rem 1rem;font-size:1.125rem;border-radius:.5rem}.btn-block{display:block;width:100%}.form-group{margin-bottom:1rem}.form-label{display:inline-block;margin-bottom:0.5rem;font-weight:700}.form-control{display:block;width:100%;padding:0.375rem 0.75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;appearance:none}.form-control:focus{color:#212529;background-color:#fff;border-color:#b3d1ef;outline:0;box-shadow:0 0 0 0.2rem rgba(74,144,217,0.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled{background-color:#f8f9fa;opacity:1}textarea.form-control{min-height:calc(1.5em + 0.75rem + 2px)}.form-select{display:block;width:100%;padding:0.375rem 2.25rem 0.375rem 0.75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;appearance:none}.form-select:focus{border-color:#b3d1ef;outline:0;box-shadow:0 0 0 0.2rem rgba(74,144,217,0.25)}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:0.125rem}.form-check-input{float:left;width:1em;height:1em;margin-left:-1.5em;margin-top:0.25em;appearance:none;background-color:#fff;border:1px solid rgba(0,0,0,0.25)}.form-check-input[type="checkbox"]{border-radius:0.25em}.form-check-input[type="radio"]{border-radius:50%}.form-check-input:checked{background-color:#4a90d9;border-color:#4a90d9}.form-check-input:focus{border-color:#b3d1ef;outline:0;box-shadow:0 0 0 0.2rem rgba(74,144,217,0.25)}.form-check-label{cursor:pointer}.is-valid{border-color:#28a745 !important}.is-valid:focus{box-shadow:0 0 0 0.2rem rgba(40,167,69,0.25) !important}.is-invalid{border-color:#dc3545 !important}.is-invalid:focus{box-shadow:0 0 0 0.2rem rgba(220,53,69,0.25) !important}.valid-feedback,.invalid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:0.875em}.valid-feedback{color:#28a745}.invalid-feedback{color:#dc3545}.is-valid~.valid-feedback,.is-invalid~.invalid-feedback{display:block}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group-text{display:flex;align-items:center;padding:0.375rem 0.75rem;font-size:1rem;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#f8f9fa;border:1px solid #ced4da;border-radius:.25rem}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:.5rem}.card-header{padding:0.75rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,0.03);border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc($border-radius-lg - 1px) calc($border-radius-lg - 1px) 0 0}.card-body{flex:1 1 auto;padding:1rem}.card-footer{padding:0.75rem 1rem;background-color:rgba(0,0,0,0.03);border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc($border-radius-lg - 1px) calc($border-radius-lg - 1px)}.card-title{margin-bottom:0.5rem;font-weight:700}.card-text:last-child{margin-bottom:0}.card-img-top{width:100%;border-top-left-radius:calc($border-radius-lg - 1px);border-top-right-radius:calc($border-radius-lg - 1px)}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:0.5rem 1rem;color:#4a90d9;text-decoration:none;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out}.nav-link:hover,.nav-link:focus{color:#256ab1}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-link.active{color:#4a90d9;font-weight:700}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:0.5rem 1rem}.navbar-brand{padding-top:0.3125rem;padding-bottom:0.3125rem;margin-right:1rem;font-size:1.25rem;font-weight:700;color:inherit;text-decoration:none;white-space:nowrap}.navbar-brand:hover{color:inherit;opacity:0.8}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.125rem;line-height:1;background-color:transparent;border:1px solid rgba(0,0,0,0.1);border-radius:.25rem;cursor:pointer}.navbar-toggler:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(74,144,217,0.25)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center;display:none}.navbar-collapse.show{display:flex}@media (min-width: 576px){.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width: 768px){.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width: 992px){.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width: 1200px){.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width: 1400px){.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}@media (min-width: 992px){.navbar-nav{flex-direction:row}.navbar-collapse{display:flex;flex-basis:auto}.navbar-toggler{display:none}}.navbar-dark{color:#fff;background-color:#212529}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .nav-link{color:rgba(255,255,255,0.75)}.navbar-dark .nav-link:hover,.navbar-dark .nav-link.active{color:#fff}.navbar-light{background-color:#f8f9fa}.navbar-light .nav-link{color:rgba(0,0,0,0.55)}.navbar-light .nav-link:hover,.navbar-light .nav-link.active{color:rgba(0,0,0,0.9)}.breadcrumb{display:flex;flex-wrap:wrap;padding:0.5rem 1rem;margin-bottom:1rem;list-style:none;background-color:#f8f9fa;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:0.5rem;padding-left:0.5rem;color:#6c757d;content:"/"}.breadcrumb-item.active{color:#6c757d}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal.show{display:block}.modal-dialog{position:relative;width:auto;margin:1.75rem auto;max-width:500px}.modal-sm{max-width:300px}.modal-lg{max-width:800px}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:.5rem;box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15);outline:0}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem;border-bottom:1px solid rgba(0,0,0,0.1)}.modal-title{margin-bottom:0;font-weight:700}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:0.75rem;border-top:1px solid rgba(0,0,0,0.1);gap:0.5rem}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:rgba(0,0,0,0.5);display:none}.modal-backdrop.show{display:block}.alert{position:relative;padding:0.75rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-primary{color:#1c5187;background-color:#deeaf8;border-color:#b3d1ef}.alert-secondary{color:#313539;background-color:#caced1;border-color:#afb5ba}.alert-success{color:#0f401b;background-color:#9be7ac;border-color:#71dd8a}.alert-danger{color:#7c151f;background-color:#f6cdd1;border-color:#efa2a9}.alert-warning{color:#876500;background-color:#ffeeba;border-color:#ffe187}.alert-info{color:#093e47;background-color:#90e4f1;border-color:#63d9ec}.alert-light{color:#aeb9c5;background-color:#fff;border-color:#fff}.alert-dark{color:#000;background-color:#717e8c;border-color:#5a6570}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;padding:0.9375rem 1rem;background:transparent;border:0;cursor:pointer;color:inherit;opacity:0.5}.alert-dismissible .btn-close:hover{opacity:0.75}.table{width:100%;margin-bottom:1rem;vertical-align:top;border-color:rgba(0,0,0,0.1)}.table>:not(caption)>*>*{padding:0.5rem;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:inherit}.table>thead{vertical-align:bottom;border-bottom:2px solid currentColor}.table>tbody>tr:last-child>*{border-bottom-color:transparent}.table-sm>:not(caption)>*>*{padding:0.25rem}.table-bordered{border:1px solid rgba(0,0,0,0.1)}.table-bordered>:not(caption)>*>*{border-width:1px;border-style:solid;border-color:inherit}.table-striped>tbody>tr:nth-of-type(odd)>*{background-color:rgba(0,0,0,0.02)}.table-hover>tbody>tr:hover>*{background-color:rgba(0,0,0,0.04)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.badge{display:inline-block;padding:0.25em 0.65em;font-size:0.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:50rem}.badge-primary{color:#fff;background-color:#4a90d9}.badge-secondary{color:#fff;background-color:#6c757d}.badge-success{color:#fff;background-color:#28a745}.badge-danger{color:#fff;background-color:#dc3545}.badge-warning{color:#212529;background-color:#ffc107}.badge-info{color:#fff;background-color:#17a2b8}.badge-light{color:#212529;background-color:#f8f9fa}.badge-dark{color:#fff;background-color:#212529}.pagination{display:flex;flex-wrap:wrap;padding-left:0;list-style:none;gap:0.25rem}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:rgba(0,0,0,0.1)}.page-item.active .page-link{color:#fff;background-color:#4a90d9;border-color:#4a90d9}.page-link{display:block;padding:0.375rem 0.75rem;color:#4a90d9;background-color:#fff;border:1px solid rgba(0,0,0,0.15);border-radius:.25rem;text-decoration:none;line-height:1.25;transition:color 0.15s, background-color 0.15s, border-color 0.15s}.page-link:hover{color:#2a76c6;background-color:rgba(74,144,217,0.08);border-color:rgba(0,0,0,0.2)}.page-link:focus{outline:0;box-shadow:0 0 0 0.2rem rgba(74,144,217,0.25)}.crud-img-preview{max-height:200px;max-width:100%;border-radius:.25rem;object-fit:cover}.crud-img-preview-sm{max-height:180px;max-width:100%;border-radius:.25rem;object-fit:cover}.d-none{display:none !important}.d-block{display:block !important}.d-flex{display:flex !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-grid{display:grid !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-block{display:block !important}.d-sm-flex{display:flex !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-block{display:block !important}.d-md-flex{display:flex !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-block{display:block !important}.d-lg-flex{display:flex !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.mx-0{margin-left:0 !important;margin-right:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.mx-1{margin-left:.25rem !important;margin-right:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.mx-2{margin-left:.5rem !important;margin-right:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.mx-3{margin-left:1rem !important;margin-right:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.mx-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.mx-5{margin-left:3rem !important;margin-right:3rem !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.px-0{padding-left:0 !important;padding-right:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.px-1{padding-left:.25rem !important;padding-right:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.px-2{padding-left:.5rem !important;padding-right:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.px-3{padding-left:1rem !important;padding-right:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.px-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.px-5{padding-left:3rem !important;padding-right:3rem !important}.mx-auto{margin-left:auto !important;margin-right:auto !important}.text-start{text-align:left !important}.text-center{text-align:center !important}.text-end{text-align:right !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fw-bold{font-weight:700 !important}.fw-normal{font-weight:400 !important}.fw-light{font-weight:300 !important}.fs-1{font-size:2.25rem !important}.fs-2{font-size:1.875rem !important}.fs-3{font-size:1.5rem !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1rem !important}.fs-6{font-size:.875rem !important}.text-primary{color:#4a90d9 !important}.text-secondary{color:#6c757d !important}.text-success{color:#28a745 !important}.text-danger{color:#dc3545 !important}.text-warning{color:#ffc107 !important}.text-info{color:#17a2b8 !important}.text-light{color:#f8f9fa !important}.text-dark{color:#212529 !important}.text-muted{color:#6c757d !important}.text-white{color:#fff !important}.bg-primary{background-color:#4a90d9 !important}.bg-secondary{background-color:#6c757d !important}.bg-success{background-color:#28a745 !important}.bg-danger{background-color:#dc3545 !important}.bg-warning{background-color:#ffc107 !important}.bg-info{background-color:#17a2b8 !important}.bg-light{background-color:#f8f9fa !important}.bg-dark{background-color:#212529 !important}.bg-white{background-color:#fff !important}.border{border:1px solid rgba(0,0,0,0.1) !important}.border-0{border:0 !important}.rounded{border-radius:.25rem !important}.rounded-0{border-radius:0 !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.w-25{width:25% !important}.h-25{height:25% !important}.w-50{width:50% !important}.h-50{height:50% !important}.w-75{width:75% !important}.h-75{height:75% !important}.w-100{width:100% !important}.h-100{height:100% !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.position-static{position:static !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.overflow-hidden{overflow:hidden !important}.overflow-auto{overflow:auto !important}.shadow{box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-sm{box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{box-shadow:none !important}.collapse{display:none}.collapse.show{display:block}.fade{opacity:0;transition:opacity 0.15s linear}.fade.show{opacity:1}.clearfix::after{display:block;clear:both;content:""}.cursor-pointer{cursor:pointer !important}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:0.25em;color:#000;background:transparent;border:0;opacity:0.5;cursor:pointer;font-size:1.25rem;line-height:1}.btn-close:hover{opacity:0.75}.btn-close:focus{opacity:1;outline:0;box-shadow:0 0 0 0.2rem rgba(74,144,217,0.25)}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:0.25rem;background-color:#fff;border:1px solid rgba(0,0,0,0.1);border-radius:.25rem;max-width:100%;height:auto}.border-top{border-top:1px solid rgba(0,0,0,0.1) !important}.border-bottom{border-bottom:1px solid rgba(0,0,0,0.1) !important}.align-middle{vertical-align:middle !important}.align-top{vertical-align:top !important}.align-bottom{vertical-align:bottom !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.visually-hidden{position:absolute !important;width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}
|
|
1
|
+
*,*::before,*::after{box-sizing:border-box}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;scroll-behavior:smooth}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.25}p{margin-top:0;margin-bottom:1rem}ol,ul{padding-left:2rem;margin-top:0;margin-bottom:1rem}a{color:#4a90d9;text-decoration:none}a:hover{color:#256ab1;text-decoration:underline}img,svg{max-width:100%;height:auto;vertical-align:middle}table{border-collapse:collapse;caption-side:bottom}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;cursor:pointer}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid rgba(0,0,0,.1);opacity:.25}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}[hidden]{display:none !important}h1{font-size:2.25rem}h2{font-size:1.875rem}h3{font-size:1.5rem}h4{font-size:1.25rem}h5{font-size:1.125rem}h6{font-size:1rem}small,.small{font-size:.875em}mark,.mark{padding:.2em;background-color:rgba(255,193,7,.3)}blockquote{margin:0 0 1rem;padding:.5rem 1rem;border-left:.25rem solid rgba(0,0,0,.1);font-size:1.125rem}code{font-size:.875em;color:#dc3545;word-wrap:break-word}pre{display:block;padding:1rem;font-size:.875em;color:#212529;background-color:#f8f9fa;border-radius:.25rem}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.container,.container-fluid{width:100%;padding-right:.75rem;padding-left:.75rem;margin-right:auto;margin-left:auto}@media(min-width: 576px){.container{max-width:540px}}@media(min-width: 768px){.container{max-width:720px}}@media(min-width: 992px){.container{max-width:960px}}@media(min-width: 1200px){.container{max-width:1140px}}@media(min-width: 1400px){.container{max-width:1320px}}.row{display:flex;flex-wrap:wrap;margin-right:-0.75rem;margin-left:-0.75rem}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:.75rem;padding-left:.75rem}.col{flex:1 0 0%}.col-1{flex:0 0 auto;width:8.3333333333%}.col-2{flex:0 0 auto;width:16.6666666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.3333333333%}.col-5{flex:0 0 auto;width:41.6666666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.3333333333%}.col-8{flex:0 0 auto;width:66.6666666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.3333333333%}.col-11{flex:0 0 auto;width:91.6666666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-0{margin-left:0}@media(min-width: 576px){.col-sm{flex:1 0 0%}.col-sm-1{flex:0 0 auto;width:8.3333333333%}.col-sm-2{flex:0 0 auto;width:16.6666666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.3333333333%}.col-sm-5{flex:0 0 auto;width:41.6666666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.3333333333%}.col-sm-8{flex:0 0 auto;width:66.6666666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.3333333333%}.col-sm-11{flex:0 0 auto;width:91.6666666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}}@media(min-width: 768px){.col-md{flex:1 0 0%}.col-md-1{flex:0 0 auto;width:8.3333333333%}.col-md-2{flex:0 0 auto;width:16.6666666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.3333333333%}.col-md-5{flex:0 0 auto;width:41.6666666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.3333333333%}.col-md-8{flex:0 0 auto;width:66.6666666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.3333333333%}.col-md-11{flex:0 0 auto;width:91.6666666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}}@media(min-width: 992px){.col-lg{flex:1 0 0%}.col-lg-1{flex:0 0 auto;width:8.3333333333%}.col-lg-2{flex:0 0 auto;width:16.6666666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.3333333333%}.col-lg-5{flex:0 0 auto;width:41.6666666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.3333333333%}.col-lg-8{flex:0 0 auto;width:66.6666666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.3333333333%}.col-lg-11{flex:0 0 auto;width:91.6666666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}}@media(min-width: 1200px){.col-xl{flex:1 0 0%}.col-xl-1{flex:0 0 auto;width:8.3333333333%}.col-xl-2{flex:0 0 auto;width:16.6666666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.3333333333%}.col-xl-5{flex:0 0 auto;width:41.6666666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.3333333333%}.col-xl-8{flex:0 0 auto;width:66.6666666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.3333333333%}.col-xl-11{flex:0 0 auto;width:91.6666666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.align-start{align-items:flex-start}.align-center{align-items:center}.align-end{align-items:flex-end}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;user-select:none;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;text-decoration:none}.btn:disabled,.btn.disabled{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#4a90d9;border-color:#4a90d9}.btn-primary:hover{background-color:#2b7bcf;border-color:#2a76c6}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{background-color:#596167;border-color:#545b62}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{background-color:#208637;border-color:#1e7e34}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{background-color:#c62232;border-color:#bd2130}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{background-color:#dda600;border-color:#d39e00}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{background-color:#128294;border-color:#117a8b}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{background-color:#e0e5e9;border-color:#dae0e5}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{background-color:#0f1112;border-color:#0a0c0d}.btn-outline-primary{color:#4a90d9;border-color:#4a90d9;background-color:rgba(0,0,0,0)}.btn-outline-primary:hover{color:#fff;background-color:#4a90d9}.btn-outline-secondary{color:#6c757d;border-color:#6c757d;background-color:rgba(0,0,0,0)}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d}.btn-outline-success{color:#28a745;border-color:#28a745;background-color:rgba(0,0,0,0)}.btn-outline-success:hover{color:#fff;background-color:#28a745}.btn-outline-danger{color:#dc3545;border-color:#dc3545;background-color:rgba(0,0,0,0)}.btn-outline-danger:hover{color:#fff;background-color:#dc3545}.btn-outline-warning{color:#ffc107;border-color:#ffc107;background-color:rgba(0,0,0,0)}.btn-outline-warning:hover{color:#212529;background-color:#ffc107}.btn-outline-info{color:#17a2b8;border-color:#17a2b8;background-color:rgba(0,0,0,0)}.btn-outline-info:hover{color:#fff;background-color:#17a2b8}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa;background-color:rgba(0,0,0,0)}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa}.btn-outline-dark{color:#212529;border-color:#212529;background-color:rgba(0,0,0,0)}.btn-outline-dark:hover{color:#fff;background-color:#212529}.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.btn-lg{padding:.5rem 1rem;font-size:1.125rem;border-radius:.5rem}.btn-block{display:block;width:100%}.form-group{margin-bottom:1rem}.form-label{display:inline-block;margin-bottom:.5rem;font-weight:700}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}.form-control:focus{color:#212529;background-color:#fff;border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled{background-color:#f8f9fa;opacity:1}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}.form-select:focus{border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check-input{float:left;width:1em;height:1em;margin-left:-1.5em;margin-top:.25em;appearance:none;background-color:#fff;border:1px solid rgba(0,0,0,.25)}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:checked{background-color:#4a90d9;border-color:#4a90d9}.form-check-input:focus{border-color:#b3d1ef;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.form-check-label{cursor:pointer}.is-valid{border-color:#28a745 !important}.is-valid:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.25) !important}.is-invalid{border-color:#dc3545 !important}.is-invalid:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.25) !important}.valid-feedback,.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em}.valid-feedback{color:#28a745}.invalid-feedback{color:#dc3545}.is-valid~.valid-feedback,.is-invalid~.invalid-feedback{display:block}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#f8f9fa;border:1px solid #ced4da;border-radius:.25rem}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.5rem}.card-header{padding:.75rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.5rem - 1px) calc(.5rem - 1px) 0 0}.card-body{flex:1 1 auto;padding:1rem}.card-footer{padding:.75rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.5rem - 1px) calc(.5rem - 1px)}.card-title{margin-bottom:.5rem;font-weight:700}.card-text:last-child{margin-bottom:0}.card-img-top{width:100%;border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#4a90d9;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out}.nav-link:hover,.nav-link:focus{color:#256ab1}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-link.active{color:#4a90d9;font-weight:700}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:.5rem 1rem}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;font-weight:700;color:inherit;text-decoration:none;white-space:nowrap}.navbar-brand:hover{color:inherit;opacity:.8}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggler{padding:.25rem .75rem;font-size:1.125rem;line-height:1;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,.1);border-radius:.25rem;cursor:pointer}.navbar-toggler:focus{outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center;display:none}.navbar-collapse.show{display:flex}@media(min-width: 576px){.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media(min-width: 768px){.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media(min-width: 992px){.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media(min-width: 1200px){.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media(min-width: 1400px){.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-collapse{display:flex;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}@media(min-width: 992px){.navbar-nav{flex-direction:row}.navbar-collapse{display:flex;flex-basis:auto}.navbar-toggler{display:none}}.navbar-dark{color:#fff;background-color:#212529}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .nav-link{color:rgba(255,255,255,.75)}.navbar-dark .nav-link:hover,.navbar-dark .nav-link.active{color:#fff}.navbar-light{background-color:#f8f9fa}.navbar-light .nav-link{color:rgba(0,0,0,.55)}.navbar-light .nav-link:hover,.navbar-light .nav-link.active{color:rgba(0,0,0,.9)}.breadcrumb{display:flex;flex-wrap:wrap;padding:.5rem 1rem;margin-bottom:1rem;list-style:none;background-color:#f8f9fa;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#6c757d;content:"/"}.breadcrumb-item.active{color:#6c757d}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal.show{display:block}.modal-dialog{position:relative;width:auto;margin:1.75rem auto;max-width:500px}.modal-sm{max-width:300px}.modal-lg{max-width:800px}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.5rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.15);outline:0}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem;border-bottom:1px solid rgba(0,0,0,.1)}.modal-title{margin-bottom:0;font-weight:700}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid rgba(0,0,0,.1);gap:.5rem}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:rgba(0,0,0,.5);display:none}.modal-backdrop.show{display:block}.alert{position:relative;padding:.75rem 1rem;margin-bottom:1rem;border:1px solid rgba(0,0,0,0);border-radius:.25rem}.alert-primary{color:#1c5187;background-color:#deeaf8;border-color:#b3d1ef}.alert-secondary{color:#313539;background-color:#caced1;border-color:#afb5ba}.alert-success{color:#0f401b;background-color:#9be7ac;border-color:#71dd8a}.alert-danger{color:#7c151f;background-color:#f6cdd1;border-color:#efa2a9}.alert-warning{color:#876500;background-color:#ffeeba;border-color:#ffe187}.alert-info{color:#093e47;background-color:#90e4f1;border-color:#63d9ec}.alert-light{color:#aeb9c5;background-color:#fff;border-color:#fff}.alert-dark{color:#000;background-color:#717e8c;border-color:#5a6570}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;padding:.9375rem 1rem;background:rgba(0,0,0,0);border:0;cursor:pointer;color:inherit;opacity:.5}.alert-dismissible .btn-close:hover{opacity:.75}.table{width:100%;margin-bottom:1rem;vertical-align:top;border-color:rgba(0,0,0,.1)}.table>:not(caption)>*>*{padding:.5rem;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:inherit}.table>thead{vertical-align:bottom;border-bottom:2px solid currentColor}.table>tbody>tr:last-child>*{border-bottom-color:rgba(0,0,0,0)}.table-sm>:not(caption)>*>*{padding:.25rem}.table-bordered{border:1px solid rgba(0,0,0,.1)}.table-bordered>:not(caption)>*>*{border-width:1px;border-style:solid;border-color:inherit}.table-striped>tbody>tr:nth-of-type(odd)>*{background-color:rgba(0,0,0,.02)}.table-hover>tbody>tr:hover>*{background-color:rgba(0,0,0,.04)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}.badge{display:inline-block;padding:.25em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:50rem}.badge-primary{color:#fff;background-color:#4a90d9}.badge-secondary{color:#fff;background-color:#6c757d}.badge-success{color:#fff;background-color:#28a745}.badge-danger{color:#fff;background-color:#dc3545}.badge-warning{color:#212529;background-color:#ffc107}.badge-info{color:#fff;background-color:#17a2b8}.badge-light{color:#212529;background-color:#f8f9fa}.badge-dark{color:#fff;background-color:#212529}.pagination{display:flex;flex-wrap:wrap;padding-left:0;list-style:none;gap:.25rem}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:rgba(0,0,0,.1)}.page-item.active .page-link{color:#fff;background-color:#4a90d9;border-color:#4a90d9}.page-link{display:block;padding:.375rem .75rem;color:#4a90d9;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;text-decoration:none;line-height:1.25;transition:color .15s,background-color .15s,border-color .15s}.page-link:hover{color:#2a76c6;background-color:rgba(74,144,217,.08);border-color:rgba(0,0,0,.2)}.page-link:focus{outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.crud-img-preview{max-height:200px;max-width:100%;border-radius:.25rem;object-fit:cover}.crud-img-preview-sm{max-height:180px;max-width:100%;border-radius:.25rem;object-fit:cover}.d-none{display:none !important}.d-block{display:block !important}.d-flex{display:flex !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-grid{display:grid !important}@media(min-width: 576px){.d-sm-none{display:none !important}.d-sm-block{display:block !important}.d-sm-flex{display:flex !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}}@media(min-width: 768px){.d-md-none{display:none !important}.d-md-block{display:block !important}.d-md-flex{display:flex !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}}@media(min-width: 992px){.d-lg-none{display:none !important}.d-lg-block{display:block !important}.d-lg-flex{display:flex !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.mx-0{margin-left:0 !important;margin-right:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.mx-1{margin-left:.25rem !important;margin-right:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.mx-2{margin-left:.5rem !important;margin-right:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.mx-3{margin-left:1rem !important;margin-right:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.mx-4{margin-left:1.5rem !important;margin-right:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.mx-5{margin-left:3rem !important;margin-right:3rem !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.px-0{padding-left:0 !important;padding-right:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.px-1{padding-left:.25rem !important;padding-right:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.px-2{padding-left:.5rem !important;padding-right:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.px-3{padding-left:1rem !important;padding-right:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.px-4{padding-left:1.5rem !important;padding-right:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.px-5{padding-left:3rem !important;padding-right:3rem !important}.mx-auto{margin-left:auto !important;margin-right:auto !important}.text-start{text-align:left !important}.text-center{text-align:center !important}.text-end{text-align:right !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fw-bold{font-weight:700 !important}.fw-normal{font-weight:400 !important}.fw-light{font-weight:300 !important}.fs-1{font-size:2.25rem !important}.fs-2{font-size:1.875rem !important}.fs-3{font-size:1.5rem !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1rem !important}.fs-6{font-size:.875rem !important}.text-primary{color:#4a90d9 !important}.text-secondary{color:#6c757d !important}.text-success{color:#28a745 !important}.text-danger{color:#dc3545 !important}.text-warning{color:#ffc107 !important}.text-info{color:#17a2b8 !important}.text-light{color:#f8f9fa !important}.text-dark{color:#212529 !important}.text-muted{color:#6c757d !important}.text-white{color:#fff !important}.bg-primary{background-color:#4a90d9 !important}.bg-secondary{background-color:#6c757d !important}.bg-success{background-color:#28a745 !important}.bg-danger{background-color:#dc3545 !important}.bg-warning{background-color:#ffc107 !important}.bg-info{background-color:#17a2b8 !important}.bg-light{background-color:#f8f9fa !important}.bg-dark{background-color:#212529 !important}.bg-white{background-color:#fff !important}.border{border:1px solid rgba(0,0,0,.1) !important}.border-0{border:0 !important}.rounded{border-radius:.25rem !important}.rounded-0{border-radius:0 !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.w-25{width:25% !important}.h-25{height:25% !important}.w-50{width:50% !important}.h-50{height:50% !important}.w-75{width:75% !important}.h-75{height:75% !important}.w-100{width:100% !important}.h-100{height:100% !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.position-static{position:static !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}.overflow-hidden{overflow:hidden !important}.overflow-auto{overflow:auto !important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}.shadow-none{box-shadow:none !important}.collapse{display:none}.collapse.show{display:block}.fade{opacity:0;transition:opacity .15s linear}.fade.show{opacity:1}.clearfix::after{display:block;clear:both;content:""}.cursor-pointer{cursor:pointer !important}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em;color:#000;background:rgba(0,0,0,0);border:0;opacity:.5;cursor:pointer;font-size:1.25rem;line-height:1}.btn-close:hover{opacity:.75}.btn-close:focus{opacity:1;outline:0;box-shadow:0 0 0 .2rem rgba(74,144,217,.25)}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.1);border-radius:.25rem;max-width:100%;height:auto}.border-top{border-top:1px solid rgba(0,0,0,.1) !important}.border-bottom{border-bottom:1px solid rgba(0,0,0,.1) !important}.align-middle{vertical-align:middle !important}.align-top{vertical-align:top !important}.align-bottom{vertical-align:bottom !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.visually-hidden{position:absolute !important;width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}
|
data/lib/tina4/rack_app.rb
CHANGED
|
@@ -19,6 +19,16 @@ module Tina4
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
class RackApp
|
|
22
|
+
class << self
|
|
23
|
+
# The process-wide RackApp — the app actually serving traffic. Set by
|
|
24
|
+
# #initialize (last one wins, the same convention as
|
|
25
|
+
# Tina4::WebSocket.current). Tina4::TestClient defaults to this so an
|
|
26
|
+
# in-process test request runs through the SAME app object a live request
|
|
27
|
+
# does, instead of building a second one with a different static root and
|
|
28
|
+
# clobbering the shared WebSocket engine. (feature-recount D6)
|
|
29
|
+
attr_accessor :current
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
STATIC_DIRS = %w[public src/public src/assets assets].freeze
|
|
23
33
|
|
|
24
34
|
# CORS is now handled by Tina4::CorsMiddleware
|
|
@@ -46,6 +56,9 @@ module Tina4
|
|
|
46
56
|
# manager. Without this the handshake never matches a route and falls
|
|
47
57
|
# through to 404, silently degrading the whole reloader to polling.
|
|
48
58
|
RackApp.register_dev_reload_ws if dev_mode?
|
|
59
|
+
|
|
60
|
+
# Publish as the process-wide app (see RackApp.current).
|
|
61
|
+
RackApp.current = self
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
# WebSocket handler for the dev-reload channel (/__dev_reload).
|
|
@@ -405,6 +418,19 @@ module Tina4
|
|
|
405
418
|
def try_static(path, env = nil)
|
|
406
419
|
return nil if path.include?("..")
|
|
407
420
|
|
|
421
|
+
# The framework ships the Swagger UI as STATIC assets under
|
|
422
|
+
# lib/tina4/public/swagger/. Static serving is independent of the gated
|
|
423
|
+
# /swagger handler, so without this check the UI stays reachable in
|
|
424
|
+
# production via '/swagger/index.html' or '/swagger/oauth2-redirect.html'
|
|
425
|
+
# even when swagger is disabled -- silently bypassing
|
|
426
|
+
# TINA4_SWAGGER_ENABLED / TINA4_DEBUG. (A bare '/swagger' and '/swagger/'
|
|
427
|
+
# are already intercepted by the gated handler, which is why this leak hid.)
|
|
428
|
+
normalised_path = path.start_with?("/") ? path : "/#{path}"
|
|
429
|
+
if (normalised_path == "/swagger" || normalised_path.start_with?("/swagger/")) &&
|
|
430
|
+
!Tina4::Swagger.enabled?
|
|
431
|
+
return nil
|
|
432
|
+
end
|
|
433
|
+
|
|
408
434
|
@static_roots.each do |root|
|
|
409
435
|
full_path = File.join(root, path)
|
|
410
436
|
if File.file?(full_path)
|
data/lib/tina4/router.rb
CHANGED
|
@@ -615,8 +615,19 @@ module Tina4
|
|
|
615
615
|
@last_routes_dir = nil
|
|
616
616
|
end
|
|
617
617
|
|
|
618
|
-
# Write a .broken sentinel
|
|
619
|
-
#
|
|
618
|
+
# Write a .broken sentinel to data/.broken/ so an auto-discover failure
|
|
619
|
+
# leaves a durable on-disk artifact instead of being swallowed into a log
|
|
620
|
+
# line.
|
|
621
|
+
#
|
|
622
|
+
# Ruby only WRITES these files today — nothing under lib/ reads them back.
|
|
623
|
+
# lib/tina4/health.rb contains no `broken` reference, and
|
|
624
|
+
# GET /__dev/api/broken serves the in-memory Tina4::DevAdmin::ErrorTracker
|
|
625
|
+
# (its own JSON store under Dir.tmpdir, dev_admin.rb:127-132), not this
|
|
626
|
+
# directory. Python's /health DOES glob data/.broken and answer 503 with
|
|
627
|
+
# `errors` + `latest_error` (tina4-python/tina4_python/core/server.py:296-320);
|
|
628
|
+
# mirroring that read side in Ruby is an OPEN parity gap. This comment used
|
|
629
|
+
# to claim "/health and the dev dashboard surface" it, which was false in
|
|
630
|
+
# both halves. (feature-recount D12)
|
|
620
631
|
def record_broken_route_import(file, error)
|
|
621
632
|
broken_dir = File.join(Dir.pwd, "data", ".broken")
|
|
622
633
|
FileUtils.mkdir_p(broken_dir) unless Dir.exist?(broken_dir)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Tina4 CSS Framework - Pagination
|
|
2
|
+
// ----------------------------------
|
|
3
|
+
|
|
4
|
+
.pagination {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-wrap: wrap;
|
|
7
|
+
padding-left: 0;
|
|
8
|
+
list-style: none;
|
|
9
|
+
gap: 0.25rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.page-item {
|
|
13
|
+
&.disabled .page-link {
|
|
14
|
+
color: $muted;
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
background-color: $body-bg;
|
|
17
|
+
border-color: rgba($black, 0.1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&.active .page-link {
|
|
21
|
+
color: $white;
|
|
22
|
+
background-color: $primary;
|
|
23
|
+
border-color: $primary;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.page-link {
|
|
28
|
+
display: block;
|
|
29
|
+
padding: 0.375rem 0.75rem;
|
|
30
|
+
color: $primary;
|
|
31
|
+
background-color: $body-bg;
|
|
32
|
+
border: 1px solid rgba($black, 0.15);
|
|
33
|
+
border-radius: $border-radius;
|
|
34
|
+
text-decoration: none;
|
|
35
|
+
line-height: 1.25;
|
|
36
|
+
transition: color 0.15s, background-color 0.15s, border-color 0.15s;
|
|
37
|
+
|
|
38
|
+
&:hover {
|
|
39
|
+
color: darken($primary, 10%);
|
|
40
|
+
background-color: rgba($primary, 0.08);
|
|
41
|
+
border-color: rgba($black, 0.2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&:focus {
|
|
45
|
+
outline: 0;
|
|
46
|
+
box-shadow: 0 0 0 0.2rem rgba($primary, 0.25);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// CRUD component image preview
|
|
51
|
+
.crud-img-preview {
|
|
52
|
+
max-height: 200px;
|
|
53
|
+
max-width: 100%;
|
|
54
|
+
border-radius: $border-radius;
|
|
55
|
+
object-fit: cover;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.crud-img-preview-sm {
|
|
59
|
+
max-height: 180px;
|
|
60
|
+
max-width: 100%;
|
|
61
|
+
border-radius: $border-radius;
|
|
62
|
+
object-fit: cover;
|
|
63
|
+
}
|
|
@@ -61,6 +61,17 @@ $spacing-dirs: (
|
|
|
61
61
|
.#{$abbr}#{$dir}-#{$key} { #{$prop}#{$suffix}: $val !important; }
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
// Axis shortcuts: py-* (top+bottom), px-* (left+right)
|
|
65
|
+
@each $key, $val in $spacers {
|
|
66
|
+
.#{$abbr}y-#{$key} {
|
|
67
|
+
#{$prop}-top: $val !important;
|
|
68
|
+
#{$prop}-bottom: $val !important;
|
|
69
|
+
}
|
|
70
|
+
.#{$abbr}x-#{$key} {
|
|
71
|
+
#{$prop}-left: $val !important;
|
|
72
|
+
#{$prop}-right: $val !important;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
.mx-auto { margin-left: auto !important; margin-right: auto !important; }
|
data/lib/tina4/scss_compiler.rb
CHANGED
|
@@ -6,6 +6,15 @@ module Tina4
|
|
|
6
6
|
SCSS_DIRS = %w[src/scss scss src/styles styles].freeze
|
|
7
7
|
CSS_OUTPUT = "src/public/css"
|
|
8
8
|
|
|
9
|
+
# Flags that may trail a variable declaration's value. `!default` means
|
|
10
|
+
# "assign only if this variable is not already set" -- the flag that makes a
|
|
11
|
+
# variable themeable. `!global` is the scope flag. Both are compiler
|
|
12
|
+
# directives: they are consumed at the declaration and must never reach the
|
|
13
|
+
# CSS, because `padding: 1.5rem !default` is invalid CSS and browsers drop
|
|
14
|
+
# the whole declaration. Sass flag names are case-SENSITIVE (`!DEFAULT` is an
|
|
15
|
+
# error in Dart Sass), so the match is deliberately case-sensitive.
|
|
16
|
+
VARIABLE_FLAG = /\s*!(default|global)\s*\z/
|
|
17
|
+
|
|
9
18
|
# Module-level state for import paths and variables
|
|
10
19
|
@import_paths = []
|
|
11
20
|
@variables = {}
|
|
@@ -92,14 +101,50 @@ module Tina4
|
|
|
92
101
|
|
|
93
102
|
private
|
|
94
103
|
|
|
104
|
+
# Split trailing !default / !global flags off a variable declaration value.
|
|
105
|
+
# Returns [value_without_flags, declares_default].
|
|
106
|
+
#
|
|
107
|
+
# Only ever called on the value of a `$name: value;` declaration, so a
|
|
108
|
+
# literal !default anywhere else -- inside a quoted string
|
|
109
|
+
# (`content: "x !default y"`) or a function argument -- is left untouched,
|
|
110
|
+
# exactly as Dart Sass leaves it. A blanket strip would corrupt real string
|
|
111
|
+
# content, and would silently turn `rgba(#000 !default, 0.1)` (a syntax
|
|
112
|
+
# error in Dart Sass) into valid-looking CSS that Sass would never emit.
|
|
113
|
+
#
|
|
114
|
+
# NOTE: this runs a regex, so it clobbers the caller's match globals --
|
|
115
|
+
# callers must capture their own groups first.
|
|
116
|
+
def strip_variable_flags(value)
|
|
117
|
+
declares_default = false
|
|
118
|
+
loop do
|
|
119
|
+
match = VARIABLE_FLAG.match(value)
|
|
120
|
+
break unless match
|
|
121
|
+
|
|
122
|
+
declares_default = true if match[1] == "default"
|
|
123
|
+
value = value[0, match.begin(0)]
|
|
124
|
+
end
|
|
125
|
+
[value.strip, declares_default]
|
|
126
|
+
end
|
|
127
|
+
|
|
95
128
|
def basic_compile(content, base_dir)
|
|
96
129
|
# Handle @import
|
|
97
130
|
content = process_imports(content, base_dir)
|
|
98
131
|
|
|
99
|
-
# Handle variables
|
|
132
|
+
# Handle variables. The /m flag makes the value span newlines, matching
|
|
133
|
+
# the Python master's `[^;]+`: without it a MULTI-LINE declaration (a map
|
|
134
|
+
# literal with its closing `) !default;` on its own line -- exactly what
|
|
135
|
+
# tina4-css's _variables.scss uses) was never extracted, and the raw SCSS
|
|
136
|
+
# was dumped verbatim into the CSS.
|
|
100
137
|
variables = {}
|
|
101
|
-
content = content.gsub(/\$([a-zA-Z_][\w-]*)\s*:\s*(.+?);/) do
|
|
102
|
-
|
|
138
|
+
content = content.gsub(/\$([a-zA-Z_][\w-]*)\s*:\s*(.+?);/m) do
|
|
139
|
+
# Capture BOTH groups before calling strip_variable_flags -- it runs a
|
|
140
|
+
# regex, which clobbers the match globals this block relies on.
|
|
141
|
+
name = Regexp.last_match(1)
|
|
142
|
+
raw = Regexp.last_match(2).strip
|
|
143
|
+
value, declares_default = strip_variable_flags(raw)
|
|
144
|
+
# !default must not overwrite a value that is already set.
|
|
145
|
+
next "" if declares_default && variables.fetch(name, "null") != "null"
|
|
146
|
+
|
|
147
|
+
variables[name] = value
|
|
103
148
|
""
|
|
104
149
|
end
|
|
105
150
|
|
data/lib/tina4/test_client.rb
CHANGED
|
@@ -47,6 +47,23 @@ module Tina4
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
class TestClient
|
|
50
|
+
# +app+ — the Rack app every request is dispatched through. Defaults to the
|
|
51
|
+
# process-wide Tina4::RackApp (RackApp.current): inside a running server that
|
|
52
|
+
# IS the app serving traffic, so an in-process test request and a live request
|
|
53
|
+
# go through the same object. With no app yet (a bare spec process) one is
|
|
54
|
+
# built lazily, rooted at +root_dir+ / Tina4.root_dir / Dir.pwd.
|
|
55
|
+
#
|
|
56
|
+
# Mirrors Node's `new TestClient(router?)` optional-collaborator constructor.
|
|
57
|
+
def initialize(app = nil, root_dir: nil)
|
|
58
|
+
@app = app
|
|
59
|
+
@root_dir = root_dir
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def app
|
|
63
|
+
@app ||= Tina4::RackApp.current ||
|
|
64
|
+
Tina4::RackApp.new(root_dir: @root_dir || Tina4.root_dir || Dir.pwd)
|
|
65
|
+
end
|
|
66
|
+
|
|
50
67
|
# Send a GET request.
|
|
51
68
|
def get(path, headers: nil)
|
|
52
69
|
request("GET", path, headers: headers)
|
|
@@ -74,7 +91,19 @@ module Tina4
|
|
|
74
91
|
|
|
75
92
|
private
|
|
76
93
|
|
|
77
|
-
# Build a
|
|
94
|
+
# Build a Rack env and dispatch it through the REAL Tina4::RackApp — the same
|
|
95
|
+
# front controller a live HTTP request goes through.
|
|
96
|
+
#
|
|
97
|
+
# This used to call Tina4::Router.match directly and invoke the handler
|
|
98
|
+
# itself, which made everything RackApp does around route matching invisible
|
|
99
|
+
# to tests: global middleware never ran, and static files, /swagger,
|
|
100
|
+
# /swagger/openapi.json, the bundled framework assets, /__dev, /__feedback,
|
|
101
|
+
# CORS preflight, the RFC 9110 OPTIONS/405 Allow responses and HEAD
|
|
102
|
+
# body-stripping all came back as a hand-built 404 {"error":"Not found"} while
|
|
103
|
+
# the live server served them 200. Any spec asserting framework endpoint
|
|
104
|
+
# behaviour through TestClient was asserting nothing. (feature-recount D6 —
|
|
105
|
+
# the same shape as the #PY2 auth fix: the in-process test client must route
|
|
106
|
+
# through the real pipeline, not a re-implementation of it.)
|
|
78
107
|
def request(method, path, json: nil, body: nil, headers: nil)
|
|
79
108
|
# Build raw body
|
|
80
109
|
raw_body = ""
|
|
@@ -99,7 +128,9 @@ module Tina4
|
|
|
99
128
|
"SERVER_PORT" => "7145",
|
|
100
129
|
"HTTP_HOST" => "localhost:7145",
|
|
101
130
|
"REMOTE_ADDR" => "127.0.0.1",
|
|
131
|
+
"SERVER_PROTOCOL" => "HTTP/1.1",
|
|
102
132
|
"rack.input" => StringIO.new(raw_body),
|
|
133
|
+
"rack.errors" => $stderr,
|
|
103
134
|
"rack.url_scheme" => "http"
|
|
104
135
|
}
|
|
105
136
|
|
|
@@ -115,57 +146,13 @@ module Tina4
|
|
|
115
146
|
end
|
|
116
147
|
end
|
|
117
148
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
route, path_params = result
|
|
126
|
-
|
|
127
|
-
# Route through the REAL auth gate (parity with the live server). A write
|
|
128
|
-
# to an auth-required route (POST/PUT/PATCH/DELETE by default, or any
|
|
129
|
-
# secured route) with no valid token / API key / formToken 401s here
|
|
130
|
-
# exactly as it would in production. The TestClient used to skip this and
|
|
131
|
-
# run the handler directly, so a green test could hide a live 401 — the
|
|
132
|
-
# verification layer lied. A public route (GET, or a write marked
|
|
133
|
-
# .no_auth) has auth_required false, so this is a no-op. (#PY2 parity)
|
|
134
|
-
unauthorized = Tina4::RackApp.enforce_route_auth(env, route)
|
|
135
|
-
return TestResponse.new(unauthorized) if unauthorized
|
|
136
|
-
|
|
137
|
-
# Create request and response
|
|
138
|
-
req = Tina4::Request.new(env, path_params || {})
|
|
139
|
-
res = Tina4::Response.new
|
|
140
|
-
|
|
141
|
-
# Build handler args (same logic as RackApp.handle_route)
|
|
142
|
-
handler_params = route.handler.parameters.map(&:last)
|
|
143
|
-
route_params = path_params || {}
|
|
144
|
-
args = handler_params.map do |name|
|
|
145
|
-
if route_params.key?(name)
|
|
146
|
-
route_params[name]
|
|
147
|
-
elsif name == :request || name == :req
|
|
148
|
-
req
|
|
149
|
-
else
|
|
150
|
-
res
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Execute handler
|
|
155
|
-
handler_result = args.empty? ? route.handler.call : route.handler.call(*args)
|
|
156
|
-
|
|
157
|
-
# Auto-detect response type
|
|
158
|
-
if handler_result.is_a?(Tina4::Response)
|
|
159
|
-
final = handler_result
|
|
160
|
-
elsif route.respond_to?(:template) && route.template && handler_result.is_a?(Hash)
|
|
161
|
-
html = Tina4::Template.render(route.template, handler_result)
|
|
162
|
-
res.html(html)
|
|
163
|
-
final = res
|
|
164
|
-
else
|
|
165
|
-
final = Tina4::Response.auto_detect(handler_result, res)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
TestResponse.new(final.to_rack)
|
|
149
|
+
# Dispatch through the real front controller. Everything the live server
|
|
150
|
+
# does — the secure-by-default auth gate (#PY2), global + per-route
|
|
151
|
+
# middleware, route matching and handler invocation, template rendering,
|
|
152
|
+
# response auto-detection, static files, /swagger, /__dev, /__feedback, the
|
|
153
|
+
# RFC 9110 OPTIONS/405 responses, HEAD body-stripping, session save and the
|
|
154
|
+
# 500 handler — is now exercised exactly once, in one place.
|
|
155
|
+
TestResponse.new(app.call(env))
|
|
169
156
|
end
|
|
170
157
|
end
|
|
171
158
|
end
|
data/lib/tina4/version.rb
CHANGED
data/lib/tina4.rb
CHANGED
|
@@ -42,7 +42,7 @@ require_relative "tina4/messenger"
|
|
|
42
42
|
require_relative "tina4/dev_mailbox"
|
|
43
43
|
require_relative "tina4/ai"
|
|
44
44
|
require_relative "tina4/cache"
|
|
45
|
-
require_relative "tina4/
|
|
45
|
+
require_relative "tina4/sql_translator"
|
|
46
46
|
require_relative "tina4/cache_backends"
|
|
47
47
|
require_relative "tina4/response_cache"
|
|
48
48
|
require_relative "tina4/html_element"
|
|
@@ -106,10 +106,25 @@ module Tina4
|
|
|
106
106
|
autoload :WebSocketBackplane, File.expand_path("tina4/websocket_backplane", __dir__)
|
|
107
107
|
autoload :RedisBackplane, File.expand_path("tina4/websocket_backplane", __dir__)
|
|
108
108
|
autoload :NATSBackplane, File.expand_path("tina4/websocket_backplane", __dir__)
|
|
109
|
+
# MQTT 3.1.1 — zero-dependency IoT client (socket + pack/unpack only).
|
|
110
|
+
# Autoloaded, not required: an app that never talks to a broker pays nothing.
|
|
111
|
+
autoload :Mqtt, File.expand_path("tina4/mqtt", __dir__)
|
|
112
|
+
autoload :MqttError, File.expand_path("tina4/mqtt", __dir__)
|
|
113
|
+
autoload :MqttTimeoutError, File.expand_path("tina4/mqtt", __dir__)
|
|
114
|
+
autoload :MqttMessage, File.expand_path("tina4/mqtt_message", __dir__)
|
|
109
115
|
autoload :Testing, File.expand_path("tina4/testing", __dir__)
|
|
110
116
|
autoload :ScssCompiler, File.expand_path("tina4/scss_compiler", __dir__)
|
|
111
117
|
autoload :FakeData, File.expand_path("tina4/seeder", __dir__)
|
|
112
118
|
autoload :WSDL, File.expand_path("tina4/wsdl", __dir__)
|
|
119
|
+
# Validator is a counted framework feature and belongs on the main
|
|
120
|
+
# `require "tina4"` surface like every other subsystem — PHP reaches
|
|
121
|
+
# Tina4\Validator through composer's autoloader, Node exports Validator from
|
|
122
|
+
# packages/core/src/index.ts, and Python resolves tina4_python.validator as a
|
|
123
|
+
# submodule with no extra wiring. Ruby was the odd one out: validator.rb was
|
|
124
|
+
# neither required nor autoloaded, so Tina4::Validator raised NameError after a
|
|
125
|
+
# clean boot. Autoloaded (not eagerly required) so it still costs nothing until
|
|
126
|
+
# a route actually validates. (feature-recount D5)
|
|
127
|
+
autoload :Validator, File.expand_path("tina4/validator", __dir__)
|
|
113
128
|
BANNER = <<~'BANNER'
|
|
114
129
|
|
|
115
130
|
______ _ __ __
|
|
@@ -141,6 +156,25 @@ module Tina4
|
|
|
141
156
|
# models with a Symbol/String `self.db` resolve against it.
|
|
142
157
|
def databases = (@databases ||= {})
|
|
143
158
|
|
|
159
|
+
# Build the startup banner's optional surface lines (issue #99).
|
|
160
|
+
#
|
|
161
|
+
# Only advertise a surface that is actually REACHABLE. In production, or with
|
|
162
|
+
# TINA4_DEBUG off, /swagger and /__dev return 404 -- printing them anyway
|
|
163
|
+
# both misleads an operator into believing a dev surface is exposed and sends
|
|
164
|
+
# a developer to a dead link.
|
|
165
|
+
#
|
|
166
|
+
# Kept as a pure function of (port, two booleans) so the contract is unit
|
|
167
|
+
# testable without booting a server and grepping stdout. Parity: Python
|
|
168
|
+
# banner_surface_lines, PHP App::bannerSurfaceLines, Node bannerSurfaceLines.
|
|
169
|
+
#
|
|
170
|
+
# @return [Array<String>] zero, one or two ready-to-puts banner rows.
|
|
171
|
+
def banner_surface_lines(port, swagger_enabled:, dev_admin_enabled:)
|
|
172
|
+
lines = []
|
|
173
|
+
lines << " Swagger: http://localhost:#{port}/swagger" if swagger_enabled
|
|
174
|
+
lines << " Dashboard: http://localhost:#{port}/__dev" if dev_admin_enabled
|
|
175
|
+
lines
|
|
176
|
+
end
|
|
177
|
+
|
|
144
178
|
def print_banner(host: "0.0.0.0", port: 7147, server_name: nil)
|
|
145
179
|
# TINA4_SUPPRESS — short-circuit ALL banner output for headless / CI runs.
|
|
146
180
|
return if Tina4::Env.is_truthy(ENV["TINA4_SUPPRESS"])
|
|
@@ -172,8 +206,12 @@ module Tina4
|
|
|
172
206
|
puts " Simple. Fast. Human. | Built for AI. Built for you."
|
|
173
207
|
puts ""
|
|
174
208
|
puts " Server: http://#{display}:#{port} (#{server_name})"
|
|
175
|
-
|
|
176
|
-
|
|
209
|
+
# Only advertise a surface that is actually reachable (issue #99).
|
|
210
|
+
banner_surface_lines(
|
|
211
|
+
port,
|
|
212
|
+
swagger_enabled: Tina4::Swagger.enabled?,
|
|
213
|
+
dev_admin_enabled: is_debug
|
|
214
|
+
).each { |line| puts line }
|
|
177
215
|
puts " Debug: #{is_debug ? 'ON' : 'OFF'} (Log level: #{log_level})"
|
|
178
216
|
puts ""
|
|
179
217
|
rescue
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.83
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -136,6 +136,34 @@ dependencies:
|
|
|
136
136
|
- - "~>"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
138
|
version: '1.8'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: logger
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '1.6'
|
|
146
|
+
type: :runtime
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '1.6'
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: base64
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - "~>"
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0.2'
|
|
160
|
+
type: :runtime
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - "~>"
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '0.2'
|
|
139
167
|
- !ruby/object:Gem::Dependency
|
|
140
168
|
name: sqlite3
|
|
141
169
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -349,6 +377,8 @@ files:
|
|
|
349
377
|
- lib/tina4/metrics.rb
|
|
350
378
|
- lib/tina4/middleware.rb
|
|
351
379
|
- lib/tina4/migration.rb
|
|
380
|
+
- lib/tina4/mqtt.rb
|
|
381
|
+
- lib/tina4/mqtt_message.rb
|
|
352
382
|
- lib/tina4/orm.rb
|
|
353
383
|
- lib/tina4/plan.rb
|
|
354
384
|
- lib/tina4/project_index.rb
|
|
@@ -396,6 +426,7 @@ files:
|
|
|
396
426
|
- lib/tina4/scss/tina4css/_grid.scss
|
|
397
427
|
- lib/tina4/scss/tina4css/_modals.scss
|
|
398
428
|
- lib/tina4/scss/tina4css/_nav.scss
|
|
429
|
+
- lib/tina4/scss/tina4css/_pagination.scss
|
|
399
430
|
- lib/tina4/scss/tina4css/_reset.scss
|
|
400
431
|
- lib/tina4/scss/tina4css/_tables.scss
|
|
401
432
|
- lib/tina4/scss/tina4css/_typography.scss
|
|
@@ -416,7 +447,7 @@ files:
|
|
|
416
447
|
- lib/tina4/session_handlers/resp_client.rb
|
|
417
448
|
- lib/tina4/session_handlers/valkey_handler.rb
|
|
418
449
|
- lib/tina4/shutdown.rb
|
|
419
|
-
- lib/tina4/
|
|
450
|
+
- lib/tina4/sql_translator.rb
|
|
420
451
|
- lib/tina4/swagger.rb
|
|
421
452
|
- lib/tina4/template.rb
|
|
422
453
|
- lib/tina4/templates/base.twig
|
|
File without changes
|