add nova
This commit is contained in:
63
nova/resources/css/app.css
Normal file
63
nova/resources/css/app.css
Normal file
@@ -0,0 +1,63 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
/*rtl:begin:ignore*/
|
||||
@import 'codemirror/lib/codemirror.css';
|
||||
@import 'codemirror/theme/3024-day.css';
|
||||
@import 'codemirror/theme/3024-night.css';
|
||||
@import 'codemirror/theme/abcdef.css';
|
||||
@import 'codemirror/theme/ambiance-mobile.css';
|
||||
@import 'codemirror/theme/ambiance.css';
|
||||
@import 'codemirror/theme/base16-dark.css';
|
||||
@import 'codemirror/theme/base16-light.css';
|
||||
@import 'codemirror/theme/bespin.css';
|
||||
@import 'codemirror/theme/blackboard.css';
|
||||
@import 'codemirror/theme/cobalt.css';
|
||||
@import 'codemirror/theme/colorforth.css';
|
||||
@import 'codemirror/theme/darcula.css';
|
||||
@import 'codemirror/theme/dracula.css';
|
||||
@import 'codemirror/theme/duotone-dark.css';
|
||||
@import 'codemirror/theme/duotone-light.css';
|
||||
@import 'codemirror/theme/eclipse.css';
|
||||
@import 'codemirror/theme/elegant.css';
|
||||
@import 'codemirror/theme/erlang-dark.css';
|
||||
@import 'codemirror/theme/gruvbox-dark.css';
|
||||
@import 'codemirror/theme/hopscotch.css';
|
||||
@import 'codemirror/theme/icecoder.css';
|
||||
@import 'codemirror/theme/idea.css';
|
||||
@import 'codemirror/theme/isotope.css';
|
||||
@import 'codemirror/theme/lesser-dark.css';
|
||||
@import 'codemirror/theme/liquibyte.css';
|
||||
@import 'codemirror/theme/lucario.css';
|
||||
@import 'codemirror/theme/material.css';
|
||||
@import 'codemirror/theme/mbo.css';
|
||||
@import 'codemirror/theme/mdn-like.css';
|
||||
@import 'codemirror/theme/midnight.css';
|
||||
@import 'codemirror/theme/monokai.css';
|
||||
@import 'codemirror/theme/neat.css';
|
||||
@import 'codemirror/theme/neo.css';
|
||||
@import 'codemirror/theme/night.css';
|
||||
@import 'codemirror/theme/oceanic-next.css';
|
||||
@import 'codemirror/theme/panda-syntax.css';
|
||||
@import 'codemirror/theme/paraiso-dark.css';
|
||||
@import 'codemirror/theme/paraiso-light.css';
|
||||
@import 'codemirror/theme/pastel-on-dark.css';
|
||||
@import 'codemirror/theme/railscasts.css';
|
||||
@import 'codemirror/theme/rubyblue.css';
|
||||
@import 'codemirror/theme/seti.css';
|
||||
@import 'codemirror/theme/shadowfox.css';
|
||||
@import 'codemirror/theme/solarized.css';
|
||||
@import 'codemirror/theme/ssms.css';
|
||||
@import 'codemirror/theme/the-matrix.css';
|
||||
@import 'codemirror/theme/tomorrow-night-bright.css';
|
||||
@import 'codemirror/theme/tomorrow-night-eighties.css';
|
||||
@import 'codemirror/theme/ttcn.css';
|
||||
@import 'codemirror/theme/twilight.css';
|
||||
@import 'codemirror/theme/vibrant-ink.css';
|
||||
@import 'codemirror/theme/xq-dark.css';
|
||||
@import 'codemirror/theme/xq-light.css';
|
||||
@import 'codemirror/theme/yeti.css';
|
||||
@import 'codemirror/theme/zenburn.css';
|
||||
/*rtl:end:ignore*/
|
||||
@import 'nova';
|
||||
@import 'fonts';
|
||||
@import 'tailwindcss/utilities';
|
||||
1098
nova/resources/css/fonts.css
Normal file
1098
nova/resources/css/fonts.css
Normal file
File diff suppressed because it is too large
Load Diff
158
nova/resources/css/form.css
Normal file
158
nova/resources/css/form.css
Normal file
@@ -0,0 +1,158 @@
|
||||
/* Form Controls
|
||||
---------------------------------------------------------------------------- */
|
||||
.form-control {
|
||||
@apply h-9 placeholder-gray-400 dark:placeholder-gray-600 leading-normal box-border focus:outline-none;
|
||||
}
|
||||
|
||||
.form-control-bordered {
|
||||
@apply ring-1 ring-gray-950/10 dark:ring-gray-100/10 focus:ring-2 focus:ring-primary-500;
|
||||
}
|
||||
|
||||
.form-control-bordered-error {
|
||||
@apply ring-red-400 dark:ring-red-500 !important;
|
||||
}
|
||||
|
||||
.form-control-focused {
|
||||
@apply ring-2 ring-primary-500;
|
||||
}
|
||||
|
||||
.form-control[data-disabled],
|
||||
.form-control:disabled {
|
||||
@apply bg-gray-50 dark:bg-gray-800 text-gray-400 outline-none;
|
||||
}
|
||||
|
||||
/* Form Inputs
|
||||
---------------------------------------------------------------------------- */
|
||||
.form-input {
|
||||
@apply appearance-none text-sm w-full bg-white dark:bg-gray-900 shadow rounded appearance-none placeholder:text-gray-400 dark:placeholder:text-gray-500 px-3 text-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
/* Form Selects
|
||||
---------------------------------------------------------------------------- */
|
||||
input[type='search'] {
|
||||
@apply pr-2;
|
||||
}
|
||||
|
||||
.dark .form-input,
|
||||
.dark input[type='search'] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.form-control + .form-select-arrow,
|
||||
.form-control > .form-select-arrow {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 11px;
|
||||
}
|
||||
|
||||
/*.form-input-row {*/
|
||||
/* @apply bg-white px-3 text-gray-600 border-0 rounded-none shadow-none h-[3rem];*/
|
||||
/*}*/
|
||||
|
||||
/*.form-select {*/
|
||||
/* @apply pl-3 pr-8;*/
|
||||
/*}*/
|
||||
|
||||
/*input.form-input:read-only,*/
|
||||
/*textarea.form-input:read-only,*/
|
||||
/*.form-input:active:disabled,*/
|
||||
/*.form-input:focus:disabled,*/
|
||||
/*.form-select:active:disabled,*/
|
||||
/*.form-select:focus:disabled {*/
|
||||
/* box-shadow: none;*/
|
||||
/*}*/
|
||||
|
||||
/*input.form-input:read-only:not([type='color']),*/
|
||||
/*textarea.form-input:read-only,*/
|
||||
/*.form-input:disabled,*/
|
||||
/*.form-input.disabled,*/
|
||||
/*.form-select:disabled {*/
|
||||
/* @apply bg-gray-50 dark:bg-gray-700;*/
|
||||
/* cursor: not-allowed;*/
|
||||
/*}*/
|
||||
|
||||
/*input.form-input[type='color']:not(:disabled) {*/
|
||||
/* cursor: pointer;*/
|
||||
/*}*
|
||||
/* Checkbox
|
||||
---------------------------------------------------------------------------- */
|
||||
.fake-checkbox {
|
||||
@apply select-none flex-shrink-0 h-4 w-4 text-primary-500 bg-white dark:bg-gray-900 rounded;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-origin: border-box;
|
||||
|
||||
@apply border border-gray-300;
|
||||
@apply dark:border-gray-700;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@apply appearance-none inline-block align-middle select-none flex-shrink-0 h-4 w-4 text-primary-500 bg-white dark:bg-gray-900 rounded;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
@apply border border-gray-300 focus:border-primary-300;
|
||||
@apply dark:border-gray-700 dark:focus:border-gray-500;
|
||||
@apply disabled:bg-gray-300 dark:disabled:bg-gray-700;
|
||||
@apply enabled:hover:cursor-pointer;
|
||||
}
|
||||
|
||||
.checkbox:focus,
|
||||
.checkbox:active {
|
||||
@apply outline-none ring-primary-200 ring-2 dark:ring-gray-700;
|
||||
}
|
||||
|
||||
.fake-checkbox-checked,
|
||||
.checkbox:checked {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M0 0h16v16H0z'/%3E%3Cpath fill='%23FFF' fill-rule='nonzero' d='M5.695 7.28A1 1 0 0 0 4.28 8.696l2 2a1 1 0 0 0 1.414 0l4-4A1 1 0 0 0 10.28 5.28L6.988 8.574 5.695 7.28Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.fake-checkbox-indeterminate,
|
||||
.checkbox:indeterminate {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M0 0h16v16H0z'/%3E%3Cpath fill='%23FFF' fill-rule='nonzero' d='M12 8a1 1 0 0 1-.883.993L11 9H5a1 1 0 0 1-.117-1.993L5 7h6a1 1 0 0 1 1 1Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
html.dark .fake-checkbox-indeterminate,
|
||||
html.dark .checkbox:indeterminate {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M0 0h16v16H0z'/%3E%3Cpath fill='%230F172A' fill-rule='nonzero' d='M12 8a1 1 0 0 1-.883.993L11 9H5a1 1 0 0 1-.117-1.993L5 7h6a1 1 0 0 1 1 1Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
@apply bg-primary-500;
|
||||
}
|
||||
|
||||
html.dark .fake-checkbox-checked,
|
||||
html.dark .checkbox:checked {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M0 0h16v16H0z'/%3E%3Cpath fill='%230F172A' fill-rule='nonzero' d='M5.695 7.28A1 1 0 0 0 4.28 8.696l2 2a1 1 0 0 0 1.414 0l4-4A1 1 0 0 0 10.28 5.28L6.988 8.574 5.695 7.28Z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
@apply bg-primary-500;
|
||||
}
|
||||
|
||||
/* File Upload
|
||||
---------------------------------------------------------------------------- */
|
||||
.form-file {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.form-file-btn {
|
||||
}
|
||||
|
||||
.form-file-input {
|
||||
@apply opacity-0 overflow-hidden absolute;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.form-file-input:focus + .form-file-btn,
|
||||
.form-file-input + .form-file-btn:hover {
|
||||
@apply bg-primary-600 cursor-pointer;
|
||||
}
|
||||
|
||||
.form-file-input:focus + .form-file-btn {
|
||||
}
|
||||
821
nova/resources/css/nova.css
Normal file
821
nova/resources/css/nova.css
Normal file
@@ -0,0 +1,821 @@
|
||||
@import 'form.css';
|
||||
|
||||
:root {
|
||||
accent-color: theme('colors.primary.500');
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute !important;
|
||||
overflow: hidden;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
}
|
||||
|
||||
.visually-hidden:is(:focus, :focus-within) + label {
|
||||
outline: thin dotted;
|
||||
}
|
||||
|
||||
/* Tooltip
|
||||
---------------------------------------------------------------------------- */
|
||||
.v-popper--theme-Nova .v-popper__inner {
|
||||
@apply shadow bg-white dark:bg-gray-900 text-gray-500 dark:text-white !important;
|
||||
}
|
||||
|
||||
.v-popper--theme-Nova .v-popper__arrow-outer {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.v-popper--theme-Nova .v-popper__arrow-inner {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.v-popper--theme-tooltip .v-popper__inner {
|
||||
@apply shadow bg-white dark:bg-gray-900 text-gray-500 dark:text-white !important;
|
||||
}
|
||||
|
||||
.v-popper--theme-tooltip .v-popper__arrow-outer {
|
||||
@apply border-white !important;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.v-popper--theme-tooltip .v-popper__arrow-inner {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Plain Theme */
|
||||
|
||||
.v-popper--theme-plain .v-popper__inner {
|
||||
@apply rounded-lg shadow bg-white dark:bg-gray-900 text-gray-500 dark:text-white !important;
|
||||
}
|
||||
|
||||
.v-popper--theme-plain .v-popper__arrow-outer {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.v-popper--theme-plain .v-popper__arrow-inner {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Help Text
|
||||
---------------------------------------------------------------------------- */
|
||||
.help-text {
|
||||
@apply text-xs leading-normal text-gray-500 italic;
|
||||
}
|
||||
|
||||
.help-text-error {
|
||||
@apply text-red-500;
|
||||
}
|
||||
|
||||
.help-text a {
|
||||
@apply text-primary-500 no-underline;
|
||||
}
|
||||
|
||||
/* Toast Messages
|
||||
-----------------------------------------------------------------------------*/
|
||||
.toasted.alive {
|
||||
padding: 0 20px;
|
||||
min-height: 38px;
|
||||
font-size: 100%;
|
||||
line-height: 1.1em;
|
||||
font-weight: 700;
|
||||
border-radius: 2px;
|
||||
background-color: #fff;
|
||||
color: #007fff;
|
||||
box-shadow: 0 12px 44px 0 rgba(10, 21, 84, 0.24);
|
||||
}
|
||||
|
||||
.toasted.alive.success {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.toasted.alive.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.toasted.alive.info {
|
||||
color: #3f51b5;
|
||||
}
|
||||
|
||||
.toasted.alive .action {
|
||||
color: #007fff;
|
||||
}
|
||||
|
||||
.toasted.alive .material-icons {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.toasted.material {
|
||||
padding: 0 20px;
|
||||
min-height: 38px;
|
||||
font-size: 100%;
|
||||
line-height: 1.1em;
|
||||
background-color: #353535;
|
||||
border-radius: 2px;
|
||||
font-weight: 300;
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.toasted.material.success {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.toasted.material.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.toasted.material.info {
|
||||
color: #3f51b5;
|
||||
}
|
||||
|
||||
.toasted.material .action {
|
||||
color: #a1c2fa;
|
||||
}
|
||||
|
||||
.toasted.colombo {
|
||||
padding: 0 20px;
|
||||
min-height: 38px;
|
||||
font-size: 100%;
|
||||
line-height: 1.1em;
|
||||
border-radius: 6px;
|
||||
color: #7492b1;
|
||||
border: 2px solid #7492b1;
|
||||
background: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.toasted.colombo:after {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #5e7b9a;
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -5px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.toasted.colombo.success {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.toasted.colombo.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.toasted.colombo.info {
|
||||
color: #3f51b5;
|
||||
}
|
||||
|
||||
.toasted.colombo .action {
|
||||
color: #007fff;
|
||||
}
|
||||
|
||||
.toasted.colombo .material-icons {
|
||||
color: #5dcccd;
|
||||
}
|
||||
|
||||
.toasted.bootstrap {
|
||||
padding: 0 20px;
|
||||
min-height: 38px;
|
||||
font-size: 100%;
|
||||
line-height: 1.1em;
|
||||
color: #31708f;
|
||||
background-color: #f9fbfd;
|
||||
border: 1px solid transparent;
|
||||
border-color: #d9edf7;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
.toasted.bootstrap.success {
|
||||
color: #3c763d;
|
||||
background-color: #dff0d8;
|
||||
border-color: #d0e9c6;
|
||||
}
|
||||
|
||||
.toasted.bootstrap.error {
|
||||
color: #a94442;
|
||||
background-color: #f2dede;
|
||||
border-color: #f2dede;
|
||||
}
|
||||
|
||||
.toasted.bootstrap.info {
|
||||
color: #31708f;
|
||||
background-color: #d9edf7;
|
||||
border-color: #d9edf7;
|
||||
}
|
||||
|
||||
.toasted.venice {
|
||||
padding: 0 20px;
|
||||
min-height: 38px;
|
||||
font-size: 100%;
|
||||
line-height: 1.1em;
|
||||
border-radius: 30px;
|
||||
color: #fff;
|
||||
background: linear-gradient(85deg, #5861bf, #a56be2);
|
||||
font-weight: 700;
|
||||
box-shadow: 0 12px 44px 0 rgba(10, 21, 84, 0.24);
|
||||
}
|
||||
|
||||
.toasted.venice.success {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.toasted.venice.error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.toasted.venice.info {
|
||||
color: #3f51b5;
|
||||
}
|
||||
|
||||
.toasted.venice .action {
|
||||
color: #007fff;
|
||||
}
|
||||
|
||||
.toasted.venice .material-icons {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.toasted.bulma {
|
||||
padding: 0 20px;
|
||||
min-height: 38px;
|
||||
font-size: 100%;
|
||||
line-height: 1.1em;
|
||||
background-color: #00d1b2;
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.toasted.bulma.success {
|
||||
color: #fff;
|
||||
background-color: #23d160;
|
||||
}
|
||||
|
||||
.toasted.bulma.error {
|
||||
color: #a94442;
|
||||
background-color: #ff3860;
|
||||
}
|
||||
|
||||
.toasted.bulma.info {
|
||||
color: #fff;
|
||||
background-color: #3273dc;
|
||||
}
|
||||
|
||||
.toasted-container {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.toasted-container,
|
||||
.toasted-container.full-width {
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toasted-container.full-width {
|
||||
max-width: 86%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toasted-container.full-width.fit-to-screen {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.toasted-container.full-width.fit-to-screen .toasted:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.toasted-container.full-width.fit-to-screen.top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.toasted-container.full-width.fit-to-screen.top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.toasted-container.full-width.fit-to-screen.top-center {
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toasted-container.full-width.fit-to-screen.bottom-right {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.toasted-container.full-width.fit-to-screen.bottom-left {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.toasted-container.full-width.fit-to-screen.bottom-center {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toasted-container.top-right {
|
||||
top: 10%;
|
||||
right: 7%;
|
||||
}
|
||||
|
||||
.toasted-container.top-right:not(.full-width) {
|
||||
-ms-flex-align: end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.toasted-container.top-left {
|
||||
top: 10%;
|
||||
left: 7%;
|
||||
}
|
||||
|
||||
.toasted-container.top-left:not(.full-width) {
|
||||
-ms-flex-align: start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.toasted-container.top-center {
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.toasted-container.bottom-right {
|
||||
right: 5%;
|
||||
bottom: 7%;
|
||||
}
|
||||
|
||||
.toasted-container.bottom-right:not(.full-width) {
|
||||
-ms-flex-align: end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.toasted-container.bottom-left {
|
||||
left: 5%;
|
||||
bottom: 7%;
|
||||
}
|
||||
|
||||
.toasted-container.bottom-left:not(.full-width) {
|
||||
-ms-flex-align: start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.toasted-container.bottom-center {
|
||||
left: 50%;
|
||||
bottom: 7%;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.toasted-container.bottom-left .toasted,
|
||||
.toasted-container.top-left .toasted {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.toasted-container.bottom-right .toasted,
|
||||
.toasted-container.top-right .toasted {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.toasted-container .toasted {
|
||||
top: 35px;
|
||||
width: auto;
|
||||
clear: both;
|
||||
margin-top: 0.8em;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
word-break: break-all;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.toasted-container .toasted .material-icons {
|
||||
margin-right: 0.5rem;
|
||||
margin-left: -0.4rem;
|
||||
}
|
||||
|
||||
.toasted-container .toasted .material-icons.after {
|
||||
margin-left: 0.5rem;
|
||||
margin-right: -0.4rem;
|
||||
}
|
||||
|
||||
.toasted-container .toasted .actions-wrapper {
|
||||
margin-left: 0.4em;
|
||||
margin-right: -1.2em;
|
||||
}
|
||||
|
||||
.toasted-container .toasted .actions-wrapper .action {
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
padding: 8px;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
.toasted-container .toasted .actions-wrapper .action.icon {
|
||||
padding: 4px;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toasted-container .toasted .actions-wrapper .action.icon .material-icons {
|
||||
margin-right: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.toasted-container .toasted .actions-wrapper .action.icon:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.toasted-container .toasted .actions-wrapper .action:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
#toasted-container {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
#toasted-container .toasted:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#toasted-container.top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#toasted-container.top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#toasted-container.top-center {
|
||||
top: 0;
|
||||
left: 0;
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#toasted-container.bottom-right {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#toasted-container.bottom-left {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#toasted-container.bottom-center {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#toasted-container.bottom-center,
|
||||
#toasted-container.top-center {
|
||||
-ms-flex-align: stretch !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
#toasted-container.bottom-left .toasted,
|
||||
#toasted-container.bottom-right .toasted,
|
||||
#toasted-container.top-left .toasted,
|
||||
#toasted-container.top-right .toasted {
|
||||
float: none;
|
||||
}
|
||||
|
||||
#toasted-container .toasted {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.toasted-container.top-center {
|
||||
top: 30px !important;
|
||||
}
|
||||
|
||||
/* TODO: Dark modes for toast messages */
|
||||
.nova {
|
||||
@apply font-bold py-2 px-5 rounded-lg shadow;
|
||||
}
|
||||
|
||||
.toasted.default {
|
||||
@apply text-primary-500 bg-primary-100 nova;
|
||||
}
|
||||
|
||||
.toasted.success {
|
||||
@apply text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900 nova;
|
||||
}
|
||||
|
||||
.toasted.error {
|
||||
@apply text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-900 nova;
|
||||
}
|
||||
|
||||
.toasted.info {
|
||||
@apply text-primary-500 dark:text-primary-400 bg-primary-50 dark:bg-primary-900 nova;
|
||||
}
|
||||
|
||||
.toasted.warning {
|
||||
@apply text-yellow-600 dark:text-yellow-900 bg-yellow-50 dark:bg-yellow-600 nova;
|
||||
}
|
||||
|
||||
.toasted .action {
|
||||
@apply font-semibold py-0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Links
|
||||
---------------------------------------------------------------------------- */
|
||||
.link-default {
|
||||
@apply no-underline text-primary-500 font-bold rounded focus:outline-none focus:ring focus:ring-primary-200;
|
||||
@apply hover:text-primary-400 active:text-primary-600;
|
||||
@apply dark:ring-gray-600;
|
||||
}
|
||||
|
||||
.link-default-error {
|
||||
@apply no-underline text-red-500 font-bold rounded focus:outline-none focus:ring focus:ring-red-200;
|
||||
@apply hover:text-red-400 active:text-red-600;
|
||||
@apply dark:ring-gray-600;
|
||||
}
|
||||
|
||||
/* Field Wrapper
|
||||
---------------------------------------------------------------------------- */
|
||||
.field-wrapper:last-child {
|
||||
@apply border-none;
|
||||
}
|
||||
|
||||
/* Chartist
|
||||
-----------------------------------------------------------------------------*/
|
||||
.chartist-tooltip {
|
||||
@apply bg-white dark:bg-gray-900 text-primary-500 rounded shadow font-sans !important;
|
||||
min-width: 0 !important;
|
||||
white-space: nowrap;
|
||||
padding: 0.2em 1em !important;
|
||||
}
|
||||
|
||||
.chartist-tooltip:before {
|
||||
display: none;
|
||||
border-top-color: rgba(var(--colors-white), 1) !important;
|
||||
}
|
||||
|
||||
/* Charts
|
||||
---------------------------------------------------------------------------- */
|
||||
/* Partition Metric */
|
||||
.ct-chart-line .ct-series-a .ct-area,
|
||||
.ct-chart-line .ct-series-a .ct-slice-donut-solid,
|
||||
.ct-chart-line .ct-series-a .ct-slice-pie {
|
||||
fill: theme('colors.primary.500') !important;
|
||||
}
|
||||
|
||||
.ct-series-b .ct-area,
|
||||
.ct-series-b .ct-slice-donut-solid,
|
||||
.ct-series-b .ct-slice-pie {
|
||||
fill: #f99037 !important;
|
||||
}
|
||||
|
||||
.ct-series-c .ct-area,
|
||||
.ct-series-c .ct-slice-donut-solid,
|
||||
.ct-series-c .ct-slice-pie {
|
||||
fill: #f2cb22 !important;
|
||||
}
|
||||
|
||||
.ct-series-d .ct-area,
|
||||
.ct-series-d .ct-slice-donut-solid,
|
||||
.ct-series-d .ct-slice-pie {
|
||||
fill: #8fc15d !important;
|
||||
}
|
||||
|
||||
.ct-series-e .ct-area,
|
||||
.ct-series-e .ct-slice-donut-solid,
|
||||
.ct-series-e .ct-slice-pie {
|
||||
fill: #098f56 !important;
|
||||
}
|
||||
|
||||
.ct-series-f .ct-area,
|
||||
.ct-series-f .ct-slice-donut-solid,
|
||||
.ct-series-f .ct-slice-pie {
|
||||
fill: #47c1bf !important;
|
||||
}
|
||||
|
||||
.ct-series-g .ct-area,
|
||||
.ct-series-g .ct-slice-donut-solid,
|
||||
.ct-series-g .ct-slice-pie {
|
||||
fill: #1693eb !important;
|
||||
}
|
||||
|
||||
.ct-series-h .ct-area,
|
||||
.ct-series-h .ct-slice-donut-solid,
|
||||
.ct-series-h .ct-slice-pie {
|
||||
fill: #6474d7 !important;
|
||||
}
|
||||
|
||||
.ct-series-i .ct-area,
|
||||
.ct-series-i .ct-slice-donut-solid,
|
||||
.ct-series-i .ct-slice-pie {
|
||||
fill: #9c6ade !important;
|
||||
}
|
||||
|
||||
.ct-series-j .ct-area,
|
||||
.ct-series-j .ct-slice-donut-solid,
|
||||
.ct-series-j .ct-slice-pie {
|
||||
fill: #e471de !important;
|
||||
}
|
||||
|
||||
/* Trend Metric */
|
||||
.ct-series-a .ct-bar,
|
||||
.ct-series-a .ct-line,
|
||||
.ct-series-a .ct-point {
|
||||
stroke: theme('colors.primary.500') !important;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.ct-series-a .ct-area,
|
||||
.ct-series-a .ct-slice-pie {
|
||||
fill: theme('colors.primary.500') !important;
|
||||
}
|
||||
|
||||
.ct-point {
|
||||
stroke: theme('colors.primary.500') !important;
|
||||
stroke-width: 6px !important;
|
||||
}
|
||||
|
||||
/* Trix
|
||||
---------------------------------------------------------------------------- */
|
||||
trix-editor {
|
||||
@apply rounded-lg dark:bg-gray-900 dark:border-gray-700;
|
||||
@apply dark:focus:bg-gray-900 focus:outline-none focus:ring ring-primary-100 dark:ring-gray-700;
|
||||
}
|
||||
|
||||
.disabled trix-editor,
|
||||
.disabled trix-toolbar {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.disabled trix-editor {
|
||||
background-color: rgba(var(--colors-gray-50), 1);
|
||||
}
|
||||
|
||||
.dark .disabled trix-editor {
|
||||
background-color: rgba(var(--colors-gray-700), 1);
|
||||
}
|
||||
|
||||
.disabled trix-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
trix-editor:empty:not(:focus)::before {
|
||||
color: rgba(var(--colors-gray-500), 1);
|
||||
}
|
||||
|
||||
trix-editor.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
trix-toolbar .trix-button-row .trix-button-group {
|
||||
@apply dark:border-gray-900;
|
||||
}
|
||||
|
||||
trix-toolbar .trix-button-row .trix-button-group .trix-button {
|
||||
@apply dark:bg-gray-400 dark:border-gray-900 dark:hover:bg-gray-300;
|
||||
}
|
||||
|
||||
trix-toolbar .trix-button-row .trix-button-group .trix-button.trix-active {
|
||||
@apply dark:bg-gray-500;
|
||||
}
|
||||
|
||||
/* Place Field
|
||||
---------------------------------------------------------------------------- */
|
||||
.modal .ap-dropdown-menu {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* KeyValue
|
||||
---------------------------------------------------------------------------- */
|
||||
.key-value-items:last-child {
|
||||
@apply rounded-b-lg bg-clip-border border-b-0;
|
||||
}
|
||||
|
||||
.key-value-items .key-value-item:last-child > .key-value-fields {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/*rtl:begin:ignore*/
|
||||
/* CodeMirror Styles
|
||||
---------------------------------------------------------------------------- */
|
||||
.CodeMirror {
|
||||
background: unset !important;
|
||||
min-height: 50px;
|
||||
font: 14px/1.5 Menlo, Consolas, Monaco, 'Andale Mono', monospace;
|
||||
box-sizing: border-box;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
color: white !important;
|
||||
@apply text-gray-500 dark:text-gray-200 !important;
|
||||
}
|
||||
|
||||
.readonly > .CodeMirror {
|
||||
@apply bg-gray-100 !important;
|
||||
}
|
||||
|
||||
.CodeMirror-wrap {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.markdown-fullscreen .markdown-content {
|
||||
height: calc(100vh - 30px);
|
||||
}
|
||||
|
||||
.markdown-fullscreen .CodeMirror {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
@apply dark:border-white;
|
||||
}
|
||||
|
||||
.cm-fat-cursor .CodeMirror-cursor {
|
||||
@apply text-black dark:text-white;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-header {
|
||||
@apply text-gray-600 dark:text-gray-300;
|
||||
}
|
||||
|
||||
/*.CodeMirror-line,*/
|
||||
.cm-s-default .cm-variable-2,
|
||||
.cm-s-default .cm-quote,
|
||||
.cm-s-default .cm-string,
|
||||
.cm-s-default .cm-comment {
|
||||
@apply text-gray-600 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.cm-s-default .cm-link,
|
||||
.cm-s-default .cm-url {
|
||||
@apply text-gray-500 dark:text-primary-400;
|
||||
}
|
||||
|
||||
/*rtl:end:ignore*/
|
||||
|
||||
/* NProgress Styles
|
||||
---------------------------------------------------------------------------- */
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: rgba(var(--colors-primary-500), 1);
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* Algolia Places Styles
|
||||
---------------------------------------------------------------------------- */
|
||||
.ap-footer-algolia svg {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.ap-footer-osm svg {
|
||||
display: inherit;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
import InlineFormData from '@/fields/Form/InlineFormData'
|
||||
|
||||
it('test it can generate proper nested attributes name', () => {
|
||||
global.FormData = class FormData {}
|
||||
|
||||
let inlineFormData = new InlineFormData('profile', new FormData())
|
||||
|
||||
expect(inlineFormData.name('email')).toEqual('profile[email]')
|
||||
expect(inlineFormData.name('email[]')).toEqual('profile[email][]')
|
||||
expect(inlineFormData.name('metadata[][filename]')).toEqual(
|
||||
'profile[metadata][][filename]'
|
||||
)
|
||||
expect(inlineFormData.name('metadata[][extension]')).toEqual(
|
||||
'profile[metadata][][extension]'
|
||||
)
|
||||
expect(inlineFormData.name('vaporFile[attribute][filename]')).toEqual(
|
||||
'profile[vaporFile][attribute][filename]'
|
||||
)
|
||||
expect(inlineFormData.name('vaporFile[attribute][extension]')).toEqual(
|
||||
'profile[vaporFile][attribute][extension]'
|
||||
)
|
||||
})
|
||||
|
||||
it('can generate proper nested attributes slug', () => {
|
||||
global.FormData = class FormData {}
|
||||
|
||||
let inlineFormData = new InlineFormData('profile', new FormData())
|
||||
|
||||
expect(inlineFormData.slug('email')).toEqual('profile.email')
|
||||
expect(inlineFormData.slug('email[]')).toEqual('profile.email[]')
|
||||
expect(inlineFormData.slug('metadata[][filename]')).toEqual(
|
||||
'profile.metadata[][filename]'
|
||||
)
|
||||
expect(inlineFormData.slug('metadata[][extension]')).toEqual(
|
||||
'profile.metadata[][extension]'
|
||||
)
|
||||
expect(inlineFormData.slug('vaporFile[attribute][filename]')).toEqual(
|
||||
'profile.vaporFile[attribute][filename]'
|
||||
)
|
||||
expect(inlineFormData.slug('vaporFile[attribute][extension]')).toEqual(
|
||||
'profile.vaporFile[attribute][extension]'
|
||||
)
|
||||
})
|
||||
75
nova/resources/js/__tests__/mixins/FieldValue.test.js
Normal file
75
nova/resources/js/__tests__/mixins/FieldValue.test.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import FieldValue from '@/mixins/FieldValue'
|
||||
|
||||
class DummyComponent {
|
||||
constructor(value) {
|
||||
this.field = {
|
||||
value: value,
|
||||
displayedAs: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('it can validate given value as integer', () => {
|
||||
let form = new DummyComponent(5)
|
||||
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 5)).toBe(true)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '5')).toBe(true)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 0)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '0')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, null)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'laravel')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'nova')).toBe(false)
|
||||
})
|
||||
|
||||
test('it can validate given value as integer (string)', () => {
|
||||
let form = new DummyComponent('5')
|
||||
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 5)).toBe(true)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '5')).toBe(true)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 0)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '0')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, null)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'laravel')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'nova')).toBe(false)
|
||||
})
|
||||
|
||||
test('it can validate given value as string', () => {
|
||||
let form = new DummyComponent('laravel')
|
||||
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 5)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '5')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 0)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '0')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, null)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'laravel')).toBe(true)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'nova')).toBe(false)
|
||||
})
|
||||
|
||||
test('it can validate given value as empty string', () => {
|
||||
let form = new DummyComponent('')
|
||||
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 5)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '5')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 0)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '0')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, null)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '')).toBe(true)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'laravel')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'nova')).toBe(false)
|
||||
})
|
||||
|
||||
test('it can validate given value as null', () => {
|
||||
let form = new DummyComponent(null)
|
||||
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 5)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '5')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 0)).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '0')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, null)).toBe(true)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, '')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'laravel')).toBe(false)
|
||||
expect(FieldValue.methods.isEqualsToValue.call(form, 'nova')).toBe(false)
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import InteractsWithDates from '@/mixins/InteractsWithDates'
|
||||
|
||||
afterAll(() => {
|
||||
delete global.Nova
|
||||
})
|
||||
|
||||
test('it can get user timezone', () => {
|
||||
global.Nova = {
|
||||
config(key) {
|
||||
return this.appConfig[key] ?? null
|
||||
},
|
||||
appConfig: {
|
||||
timezone: 'UTC',
|
||||
userTimezone: 'Asia/Kuala_Lumpur',
|
||||
},
|
||||
}
|
||||
|
||||
expect(InteractsWithDates.computed.userTimezone()).toBe('Asia/Kuala_Lumpur')
|
||||
})
|
||||
|
||||
test('it can fallback to application timezone if user does not define timezone', () => {
|
||||
global.Nova = {
|
||||
config(key) {
|
||||
return this.appConfig[key] ?? null
|
||||
},
|
||||
appConfig: {
|
||||
timezone: 'UTC',
|
||||
},
|
||||
}
|
||||
|
||||
expect(InteractsWithDates.computed.userTimezone()).toBe('UTC')
|
||||
})
|
||||
27
nova/resources/js/__tests__/mixins/packages.test.js
Normal file
27
nova/resources/js/__tests__/mixins/packages.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useLocalization } from '@/mixins/packages'
|
||||
|
||||
afterAll(() => {
|
||||
delete global.Nova
|
||||
})
|
||||
|
||||
test('it can use localization', () => {
|
||||
const { __ } = useLocalization()
|
||||
|
||||
global.Nova = {
|
||||
config(key) {
|
||||
return this.appConfig[key] ?? null
|
||||
},
|
||||
appConfig: {
|
||||
translations: {
|
||||
taylorotwell: 'Taylor Otwell',
|
||||
'Laravel Nova :version': 'Laravel Nova v:version',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(__('taylorotwell')).toBe('Taylor Otwell')
|
||||
expect(__('Laravel Nova')).toBe('Laravel Nova')
|
||||
expect(__('Laravel Nova :version', { version: '4.0.0' })).toBe(
|
||||
'Laravel Nova v4.0.0'
|
||||
)
|
||||
})
|
||||
13
nova/resources/js/__tests__/util/hourCycle.test.js
Normal file
13
nova/resources/js/__tests__/util/hourCycle.test.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { default as hourCycle } from '@/util/hourCycle'
|
||||
|
||||
it('can uses 12 hour cycles', () => {
|
||||
expect(hourCycle('en-US')).toEqual(12)
|
||||
expect(hourCycle('ms-MY')).toEqual(12)
|
||||
expect(hourCycle('ko-KR')).toEqual(12)
|
||||
expect(hourCycle('ar-EG')).toEqual(12)
|
||||
})
|
||||
|
||||
it('can uses 24 hour cycles', () => {
|
||||
expect(hourCycle('en-GB')).toEqual(24)
|
||||
expect(hourCycle('ja-JP')).toEqual(24)
|
||||
})
|
||||
17
nova/resources/js/__tests__/util/increaseOrDecrease.test.js
Normal file
17
nova/resources/js/__tests__/util/increaseOrDecrease.test.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import increaseOrDecrease from '@/util/increaseOrDecrease'
|
||||
|
||||
test('it can calculate increase in percentage', () => {
|
||||
expect(increaseOrDecrease(50, 0)).toBe(null)
|
||||
expect(increaseOrDecrease(45, 10)).toBe(350)
|
||||
expect(increaseOrDecrease(45, 36)).toBe(25)
|
||||
expect(increaseOrDecrease(45, 40)).toBe(12.5)
|
||||
expect(increaseOrDecrease(50, -50)).toBe(200)
|
||||
})
|
||||
|
||||
test('it can calculate decrease in percentage', () => {
|
||||
expect(increaseOrDecrease(0, 50)).toBe(-100)
|
||||
expect(increaseOrDecrease(10, 45)).toBe(-77.77777777777779)
|
||||
expect(increaseOrDecrease(36, 45)).toBe(-20)
|
||||
expect(increaseOrDecrease(40, 45)).toBe(-11.11111111111111)
|
||||
expect(increaseOrDecrease(-50, 50)).toBe(-200)
|
||||
})
|
||||
14
nova/resources/js/__tests__/util/singularOrPlural.test.js
Normal file
14
nova/resources/js/__tests__/util/singularOrPlural.test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import singularOrPlural from '@/util/singularOrPlural'
|
||||
|
||||
test('it can return correct inflector results', () => {
|
||||
expect(singularOrPlural(0, 'hour')).toBe('hours')
|
||||
expect(singularOrPlural(1, 'hour')).toBe('hour')
|
||||
expect(singularOrPlural(1.23, 'hour')).toBe('hours')
|
||||
expect(singularOrPlural(40, 'hour')).toBe('hours')
|
||||
expect(singularOrPlural(40, 'Bouqueté')).toBe('Bouquetés')
|
||||
})
|
||||
|
||||
test('it does ignore when suffix is a symbol', () => {
|
||||
expect(singularOrPlural(40, '%')).toBe('%')
|
||||
expect(singularOrPlural(40, '!')).toBe('!')
|
||||
})
|
||||
132
nova/resources/js/__tests__/util/tailwindConfig.test.js
Normal file
132
nova/resources/js/__tests__/util/tailwindConfig.test.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
generateRootCSSVars,
|
||||
generateTailwindColors,
|
||||
} from '../../../../generators'
|
||||
|
||||
it('generates Tailwind colors', () => {
|
||||
expect(generateTailwindColors()).toEqual(
|
||||
expect.objectContaining({
|
||||
current: 'currentColor',
|
||||
inherit: 'inherit',
|
||||
transparent: 'transparent',
|
||||
black: '#000',
|
||||
white: '#fff',
|
||||
primary: {
|
||||
100: 'rgba(var(--colors-primary-100))',
|
||||
200: 'rgba(var(--colors-primary-200))',
|
||||
300: 'rgba(var(--colors-primary-300))',
|
||||
400: 'rgba(var(--colors-primary-400))',
|
||||
50: 'rgba(var(--colors-primary-50))',
|
||||
500: 'rgba(var(--colors-primary-500))',
|
||||
600: 'rgba(var(--colors-primary-600))',
|
||||
700: 'rgba(var(--colors-primary-700))',
|
||||
800: 'rgba(var(--colors-primary-800))',
|
||||
900: 'rgba(var(--colors-primary-900))',
|
||||
950: 'rgba(var(--colors-primary-950))',
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const data = {
|
||||
lightBlue: {
|
||||
100: 'rgba(var(--colors-lightBlue-100))',
|
||||
200: 'rgba(var(--colors-lightBlue-200))',
|
||||
300: 'rgba(var(--colors-lightBlue-300))',
|
||||
400: 'rgba(var(--colors-lightBlue-400))',
|
||||
50: 'rgba(var(--colors-lightBlue-50))',
|
||||
500: 'rgba(var(--colors-lightBlue-500))',
|
||||
600: 'rgba(var(--colors-lightBlue-600))',
|
||||
700: 'rgba(var(--colors-lightBlue-700))',
|
||||
800: 'rgba(var(--colors-lightBlue-800))',
|
||||
900: 'rgba(var(--colors-lightBlue-900))',
|
||||
},
|
||||
|
||||
warmGray: {
|
||||
100: 'rgba(var(--colors-warmGray-100))',
|
||||
200: 'rgba(var(--colors-warmGray-200))',
|
||||
300: 'rgba(var(--colors-warmGray-300))',
|
||||
400: 'rgba(var(--colors-warmGray-400))',
|
||||
50: 'rgba(var(--colors-warmGray-50))',
|
||||
500: 'rgba(var(--colors-warmGray-500))',
|
||||
600: 'rgba(var(--colors-warmGray-600))',
|
||||
700: 'rgba(var(--colors-warmGray-700))',
|
||||
800: 'rgba(var(--colors-warmGray-800))',
|
||||
900: 'rgba(var(--colors-warmGray-900))',
|
||||
},
|
||||
|
||||
trueGray: {
|
||||
100: 'rgba(var(--colors-trueGray-100))',
|
||||
200: 'rgba(var(--colors-trueGray-200))',
|
||||
300: 'rgba(var(--colors-trueGray-300))',
|
||||
400: 'rgba(var(--colors-trueGray-400))',
|
||||
50: 'rgba(var(--colors-trueGray-50))',
|
||||
500: 'rgba(var(--colors-trueGray-500))',
|
||||
600: 'rgba(var(--colors-trueGray-600))',
|
||||
700: 'rgba(var(--colors-trueGray-700))',
|
||||
800: 'rgba(var(--colors-trueGray-800))',
|
||||
900: 'rgba(var(--colors-trueGray-900))',
|
||||
},
|
||||
|
||||
coolGray: {
|
||||
100: 'rgba(var(--colors-coolGray-100))',
|
||||
200: 'rgba(var(--colors-coolGray-200))',
|
||||
300: 'rgba(var(--colors-coolGray-300))',
|
||||
400: 'rgba(var(--colors-coolGray-400))',
|
||||
50: 'rgba(var(--colors-coolGray-50))',
|
||||
500: 'rgba(var(--colors-coolGray-500))',
|
||||
600: 'rgba(var(--colors-coolGray-600))',
|
||||
700: 'rgba(var(--colors-coolGray-700))',
|
||||
800: 'rgba(var(--colors-coolGray-800))',
|
||||
900: 'rgba(var(--colors-coolGray-900))',
|
||||
},
|
||||
|
||||
blueGray: {
|
||||
100: 'rgba(var(--colors-blueGray-100))',
|
||||
200: 'rgba(var(--colors-blueGray-200))',
|
||||
300: 'rgba(var(--colors-blueGray-300))',
|
||||
400: 'rgba(var(--colors-blueGray-400))',
|
||||
50: 'rgba(var(--colors-blueGray-50))',
|
||||
500: 'rgba(var(--colors-blueGray-500))',
|
||||
600: 'rgba(var(--colors-blueGray-600))',
|
||||
700: 'rgba(var(--colors-blueGray-700))',
|
||||
800: 'rgba(var(--colors-blueGray-800))',
|
||||
900: 'rgba(var(--colors-blueGray-900))',
|
||||
},
|
||||
}
|
||||
|
||||
describe.each(Object.keys(data))(
|
||||
`It does not generate the deprecated Tailwind colors`,
|
||||
key => {
|
||||
it(`does not generate "${key}" colors`, () => {
|
||||
expect(generateTailwindColors()).toEqual(
|
||||
expect.not.objectContaining({ [key]: data[key] })
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('generates root CSS variables', () => {
|
||||
expect(generateRootCSSVars()).toEqual(
|
||||
expect.objectContaining({
|
||||
'--colors-primary-50': '240, 249, 255',
|
||||
'--colors-primary-100': '224, 242, 254',
|
||||
'--colors-primary-200': '186, 230, 253',
|
||||
'--colors-primary-300': '125, 211, 252',
|
||||
'--colors-primary-400': '56, 189, 248',
|
||||
'--colors-primary-500': '14, 165, 233',
|
||||
'--colors-primary-600': '2, 132, 199',
|
||||
'--colors-primary-700': '3, 105, 161',
|
||||
'--colors-primary-800': '7, 89, 133',
|
||||
'--colors-primary-900': '12, 74, 110',
|
||||
})
|
||||
)
|
||||
|
||||
expect(generateRootCSSVars()).toEqual(
|
||||
expect.not.objectContaining({
|
||||
'--colors-inherit': 'inherit',
|
||||
'--colors-current': 'current',
|
||||
'--colors-transparent': 'transparent',
|
||||
})
|
||||
)
|
||||
})
|
||||
14
nova/resources/js/__tests__/util/url.test.js
Normal file
14
nova/resources/js/__tests__/util/url.test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import url from '@/util/url'
|
||||
|
||||
it('it can generate proper urls', () => {
|
||||
expect(url('nova', '/resources/users')).toEqual('nova/resources/users')
|
||||
expect(url('nova', '/resources/users', { users_per_page: 15 })).toEqual(
|
||||
'nova/resources/users?users_per_page=15'
|
||||
)
|
||||
expect(
|
||||
url('nova', '/resources/users', { search: 'nova', users_per_page: 15 })
|
||||
).toEqual('nova/resources/users?search=nova&users_per_page=15')
|
||||
expect(url('nova', '/resources/users', { resources: [1, 2, 3] })).toEqual(
|
||||
'nova/resources/users?resources=1%2C2%2C3'
|
||||
)
|
||||
})
|
||||
75
nova/resources/js/__tests__/vendor/luxon.test.js
vendored
Normal file
75
nova/resources/js/__tests__/vendor/luxon.test.js
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
it('can handle UTC datetime', () => {
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-14T02:48:15+00:00')
|
||||
.setZone('UTC')
|
||||
.toISO()
|
||||
).toEqual('2021-10-14T02:48:15.000Z')
|
||||
})
|
||||
|
||||
it('can convert datetime from UTC', () => {
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-14T02:48:15+00:00')
|
||||
.setZone('America/Chicago')
|
||||
.toISO()
|
||||
).toEqual('2021-10-13T21:48:15.000-05:00')
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-14T02:48:15+00:00')
|
||||
.setZone('America/Mexico_City')
|
||||
.toISO()
|
||||
).toEqual('2021-10-13T21:48:15.000-05:00')
|
||||
expect(
|
||||
DateTime.fromISO('2023-05-02T14:00:00+00:00')
|
||||
.setZone('America/Mexico_City')
|
||||
.toISO()
|
||||
).toEqual('2023-05-02T08:00:00.000-06:00')
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-14T02:48:15+00:00')
|
||||
.setZone('Europe/Paris')
|
||||
.toISO()
|
||||
).toEqual('2021-10-14T04:48:15.000+02:00')
|
||||
expect(
|
||||
DateTime.fromISO('2022-05-10T10:00:00+00:00')
|
||||
.setZone('Europe/Paris')
|
||||
.toISO()
|
||||
).toEqual('2022-05-10T12:00:00.000+02:00')
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-14T02:48:15+00:00')
|
||||
.setZone('Asia/Kuala_Lumpur')
|
||||
.toISO()
|
||||
).toEqual('2021-10-14T10:48:15.000+08:00')
|
||||
})
|
||||
|
||||
it('can convert datetime to UTC', () => {
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-13T21:48:15.000-05:00', { zone: 'America/Chicago' })
|
||||
.setZone('UTC')
|
||||
.toISO()
|
||||
).toEqual('2021-10-14T02:48:15.000Z')
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-13T21:48:15.000-05:00', { zone: 'America/Mexico_City' })
|
||||
.setZone('UTC')
|
||||
.toISO()
|
||||
).toEqual('2021-10-14T02:48:15.000Z')
|
||||
expect(
|
||||
DateTime.fromISO('2023-05-02T08:00:00.000-06:00', { zone: 'America/Mexico_City' })
|
||||
.setZone('UTC')
|
||||
.toISO()
|
||||
).toEqual('2023-05-02T14:00:00.000Z')
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-14T04:48:15.000+02:00', { zone: 'Europe/Paris' })
|
||||
.setZone('UTC')
|
||||
.toISO()
|
||||
).toEqual('2021-10-14T02:48:15.000Z')
|
||||
expect(
|
||||
DateTime.fromISO('2022-05-10T12:00:00.000+02:00', { zone: 'Europe/Paris' })
|
||||
.setZone('UTC')
|
||||
.toISO()
|
||||
).toEqual('2022-05-10T10:00:00.000Z')
|
||||
expect(
|
||||
DateTime.fromISO('2021-10-14T10:48:15.000+08:00', { zone: 'Asia/Kuala_Lumpur' })
|
||||
.setZone('UTC')
|
||||
.toISO()
|
||||
).toEqual('2021-10-14T02:48:15.000Z')
|
||||
})
|
||||
515
nova/resources/js/app.js
Normal file
515
nova/resources/js/app.js
Normal file
@@ -0,0 +1,515 @@
|
||||
import Localization from '@/mixins/Localization'
|
||||
import { setupAxios } from '@/util/axios'
|
||||
import { setupNumbro } from '@/util/numbro'
|
||||
import { setupInertia } from '@/util/inertia'
|
||||
import url from '@/util/url'
|
||||
import { createInertiaApp, Head, Link } from '@inertiajs/inertia-vue3'
|
||||
import { Inertia } from '@inertiajs/inertia'
|
||||
import NProgress from 'nprogress'
|
||||
import { registerViews } from './components'
|
||||
import { registerFields } from './fields'
|
||||
import Mousetrap from 'mousetrap'
|
||||
import Form from 'form-backend-validation'
|
||||
import { createNovaStore } from './store'
|
||||
import resourceStore from './store/resources'
|
||||
import FloatingVue from 'floating-vue'
|
||||
import find from 'lodash/find'
|
||||
import isNil from 'lodash/isNil'
|
||||
import fromPairs from 'lodash/fromPairs'
|
||||
import isString from 'lodash/isString'
|
||||
import omit from 'lodash/omit'
|
||||
import Toasted from 'toastedjs'
|
||||
import Emitter from 'tiny-emitter'
|
||||
import Layout from '@/layouts/AppLayout'
|
||||
import CodeMirror from 'codemirror'
|
||||
import { Settings } from 'luxon'
|
||||
import 'codemirror/mode/markdown/markdown'
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/mode/php/php'
|
||||
import 'codemirror/mode/ruby/ruby'
|
||||
import 'codemirror/mode/shell/shell'
|
||||
import 'codemirror/mode/sass/sass'
|
||||
import 'codemirror/mode/yaml/yaml'
|
||||
import 'codemirror/mode/yaml-frontmatter/yaml-frontmatter'
|
||||
import 'codemirror/mode/nginx/nginx'
|
||||
import 'codemirror/mode/xml/xml'
|
||||
import 'codemirror/mode/vue/vue'
|
||||
import 'codemirror/mode/dockerfile/dockerfile'
|
||||
import 'codemirror/keymap/vim'
|
||||
import 'codemirror/mode/sql/sql'
|
||||
import 'codemirror/mode/twig/twig'
|
||||
import 'codemirror/mode/htmlmixed/htmlmixed'
|
||||
import { ColorTranslator } from 'colortranslator'
|
||||
|
||||
import 'floating-vue/dist/style.css'
|
||||
|
||||
const { parseColor } = require('tailwindcss/lib/util/color')
|
||||
|
||||
CodeMirror.defineMode('htmltwig', function (config, parserConfig) {
|
||||
return CodeMirror.overlayMode(
|
||||
CodeMirror.getMode(config, parserConfig.backdrop || 'text/html'),
|
||||
CodeMirror.getMode(config, 'twig')
|
||||
)
|
||||
})
|
||||
|
||||
const emitter = new Emitter()
|
||||
|
||||
window.createNovaApp = config => new Nova(config)
|
||||
window.Vue = require('vue')
|
||||
|
||||
const { createApp, h } = window.Vue
|
||||
|
||||
class Nova {
|
||||
constructor(config) {
|
||||
this.bootingCallbacks = []
|
||||
this.appConfig = config
|
||||
this.useShortcuts = true
|
||||
|
||||
this.pages = {
|
||||
'Nova.Attach': require('@/pages/Attach').default,
|
||||
'Nova.Create': require('@/pages/Create').default,
|
||||
'Nova.Dashboard': require('@/pages/Dashboard').default,
|
||||
'Nova.Detail': require('@/pages/Detail').default,
|
||||
'Nova.Error': require('@/pages/AppError').default,
|
||||
'Nova.Error403': require('@/pages/Error403').default,
|
||||
'Nova.Error404': require('@/pages/Error404').default,
|
||||
'Nova.ForgotPassword': require('@/pages/ForgotPassword').default,
|
||||
'Nova.Index': require('@/pages/Index').default,
|
||||
'Nova.Lens': require('@/pages/Lens').default,
|
||||
'Nova.Login': require('@/pages/Login').default,
|
||||
'Nova.Replicate': require('@/pages/Replicate').default,
|
||||
'Nova.ResetPassword': require('@/pages/ResetPassword').default,
|
||||
'Nova.Update': require('@/pages/Update').default,
|
||||
'Nova.UpdateAttached': require('@/pages/UpdateAttached').default,
|
||||
}
|
||||
|
||||
this.$toasted = new Toasted({
|
||||
theme: 'nova',
|
||||
position: config.rtlEnabled ? 'bottom-left' : 'bottom-right',
|
||||
duration: 6000,
|
||||
})
|
||||
this.$progress = NProgress
|
||||
this.$router = Inertia
|
||||
|
||||
if (config.debug === true) {
|
||||
this.$testing = {
|
||||
timezone: timezone => {
|
||||
Settings.defaultZoneName = timezone
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be called before Nova starts. This is used to bootstrap
|
||||
* addons, tools, custom fields, or anything else Nova needs
|
||||
*/
|
||||
booting(callback) {
|
||||
this.bootingCallbacks.push(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all of the booting callbacks.
|
||||
*/
|
||||
boot() {
|
||||
this.store = createNovaStore()
|
||||
|
||||
this.bootingCallbacks.forEach(callback => callback(this.app, this.store))
|
||||
this.bootingCallbacks = []
|
||||
}
|
||||
|
||||
booted(callback) {
|
||||
callback(this.app, this.store)
|
||||
}
|
||||
|
||||
async countdown() {
|
||||
this.log('Initiating Nova countdown...')
|
||||
|
||||
const appName = this.config('appName')
|
||||
|
||||
await createInertiaApp({
|
||||
title: title => (!title ? appName : `${title} - ${appName}`),
|
||||
resolve: name => {
|
||||
const page = !isNil(this.pages[name])
|
||||
? this.pages[name]
|
||||
: require('@/pages/Error404').default
|
||||
|
||||
page.layout = page.layout || Layout
|
||||
|
||||
return page
|
||||
},
|
||||
setup: ({ el, App, props, plugin }) => {
|
||||
this.mountTo = el
|
||||
this.app = createApp({ render: () => h(App, props) })
|
||||
|
||||
this.app.use(plugin)
|
||||
this.app.use(FloatingVue, {
|
||||
preventOverflow: true,
|
||||
flip: true,
|
||||
themes: {
|
||||
Nova: {
|
||||
$extend: 'tooltip',
|
||||
triggers: ['click'],
|
||||
autoHide: true,
|
||||
placement: 'bottom',
|
||||
html: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Nova app by calling each of the tool's callbacks and then creating
|
||||
* the underlying Vue instance.
|
||||
*/
|
||||
liftOff() {
|
||||
this.log('We have lift off!')
|
||||
|
||||
this.boot()
|
||||
|
||||
if (this.config('notificationCenterEnabled')) {
|
||||
this.notificationPollingInterval = setInterval(() => {
|
||||
if (document.hasFocus()) {
|
||||
this.$emit('refresh-notifications')
|
||||
}
|
||||
}, this.config('notificationPollingInterval'))
|
||||
}
|
||||
|
||||
this.registerStoreModules()
|
||||
|
||||
this.app.mixin(Localization)
|
||||
|
||||
setupInertia()
|
||||
|
||||
document.addEventListener('inertia:before', () => {
|
||||
;(async () => {
|
||||
this.log('Syncing Inertia props to the store...')
|
||||
await this.store.dispatch('assignPropsFromInertia')
|
||||
})()
|
||||
})
|
||||
|
||||
document.addEventListener('inertia:navigate', () => {
|
||||
;(async () => {
|
||||
this.log('Syncing Inertia props to the store...')
|
||||
await this.store.dispatch('assignPropsFromInertia')
|
||||
})()
|
||||
})
|
||||
|
||||
this.app.mixin({
|
||||
methods: {
|
||||
$url: (path, parameters) => this.url(path, parameters),
|
||||
},
|
||||
})
|
||||
|
||||
this.component('Link', Link)
|
||||
this.component('InertiaLink', Link)
|
||||
this.component('Head', Head)
|
||||
|
||||
registerViews(this)
|
||||
registerFields(this)
|
||||
|
||||
this.app.mount(this.mountTo)
|
||||
|
||||
let mousetrapDefaultStopCallback = Mousetrap.prototype.stopCallback
|
||||
|
||||
Mousetrap.prototype.stopCallback = (e, element, combo) => {
|
||||
if (!this.useShortcuts) {
|
||||
return true
|
||||
}
|
||||
|
||||
return mousetrapDefaultStopCallback.call(this, e, element, combo)
|
||||
}
|
||||
|
||||
Mousetrap.init()
|
||||
|
||||
this.applyTheme()
|
||||
|
||||
this.log('All systems go...')
|
||||
}
|
||||
|
||||
config(key) {
|
||||
return this.appConfig[key]
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a form object configured with Nova's preconfigured axios instance.
|
||||
*
|
||||
* @param {object} data
|
||||
*/
|
||||
form(data) {
|
||||
return new Form(data, {
|
||||
http: this.request(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an axios instance configured to make requests to Nova's API
|
||||
* and handle certain response codes.
|
||||
*/
|
||||
request(options) {
|
||||
let axios = setupAxios()
|
||||
|
||||
if (options !== undefined) {
|
||||
return axios(options)
|
||||
}
|
||||
|
||||
return axios
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL from base Nova prefix.
|
||||
*/
|
||||
url(path, parameters) {
|
||||
if (path === '/') {
|
||||
path = this.config('initialPath')
|
||||
}
|
||||
|
||||
return url(this.config('base'), path, parameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener on Nova's built-in event bus
|
||||
*/
|
||||
$on(...args) {
|
||||
emitter.on(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a one-time listener on the event bus
|
||||
*/
|
||||
$once(...args) {
|
||||
emitter.once(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an listener on the event bus
|
||||
*/
|
||||
$off(...args) {
|
||||
emitter.off(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event on the event bus
|
||||
*/
|
||||
$emit(...args) {
|
||||
emitter.emit(...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if Nova is missing the requested resource with the given uri key
|
||||
*/
|
||||
missingResource(uriKey) {
|
||||
return (
|
||||
find(this.config('resources'), r => r.uriKey === uriKey) === undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a keyboard shortcut.
|
||||
*/
|
||||
addShortcut(keys, callback) {
|
||||
Mousetrap.bind(keys, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbind a keyboard shortcut.
|
||||
*/
|
||||
disableShortcut(keys) {
|
||||
Mousetrap.unbind(keys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause all keyboard shortcuts.
|
||||
*/
|
||||
pauseShortcuts() {
|
||||
this.useShortcuts = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume all keyboard shortcuts.
|
||||
*/
|
||||
resumeShortcuts() {
|
||||
this.useShortcuts = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the built-in Vuex modules for each resource
|
||||
*/
|
||||
registerStoreModules() {
|
||||
this.app.use(this.store)
|
||||
|
||||
this.config('resources').forEach(resource => {
|
||||
this.store.registerModule(resource.uriKey, resourceStore)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Inertia component.
|
||||
*/
|
||||
inertia(name, component) {
|
||||
this.pages[name] = component
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom Vue component.
|
||||
*/
|
||||
component(name, component) {
|
||||
if (isNil(this.app._context.components[name])) {
|
||||
this.app.component(name, component)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error message to the user.
|
||||
*
|
||||
* @param {string} message
|
||||
*/
|
||||
info(message) {
|
||||
this.$toasted.show(message, { type: 'info' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an error message to the user.
|
||||
*
|
||||
* @param {string} message
|
||||
*/
|
||||
error(message) {
|
||||
this.$toasted.show(message, { type: 'error' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success message to the user.
|
||||
*
|
||||
* @param {string} message
|
||||
*/
|
||||
success(message) {
|
||||
this.$toasted.show(message, { type: 'success' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a warning message to the user.
|
||||
*
|
||||
* @param {string} message
|
||||
*/
|
||||
warning(message) {
|
||||
this.$toasted.show(message, { type: 'warning' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number using numbro.js for consistent number formatting.
|
||||
*/
|
||||
formatNumber(number, format) {
|
||||
const numbro = setupNumbro(
|
||||
document.querySelector('meta[name="locale"]').content
|
||||
)
|
||||
const num = numbro(number)
|
||||
|
||||
if (format !== undefined) {
|
||||
return num.format(format)
|
||||
}
|
||||
|
||||
return num.format()
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to the console with the NOVA prefix
|
||||
*
|
||||
* @param message
|
||||
* @param type
|
||||
*/
|
||||
log(message, type = 'log') {
|
||||
console[type](`[NOVA]`, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to login path.
|
||||
*/
|
||||
redirectToLogin() {
|
||||
const url =
|
||||
!this.config('withAuthentication') && this.config('customLoginPath')
|
||||
? this.config('customLoginPath')
|
||||
: this.url('/login')
|
||||
|
||||
this.visit({
|
||||
remote: true,
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit page using Inertia visit or window.location for remote.
|
||||
*/
|
||||
visit(path, options) {
|
||||
options = options || {}
|
||||
const openInNewTab = options?.openInNewTab || null
|
||||
|
||||
if (isString(path)) {
|
||||
Inertia.visit(this.url(path), omit(options, ['openInNewTab']))
|
||||
return
|
||||
}
|
||||
|
||||
if (isString(path.url) && path.hasOwnProperty('remote')) {
|
||||
if (path.remote === true) {
|
||||
if (openInNewTab === true) {
|
||||
window.open(path.url, '_blank')
|
||||
} else {
|
||||
window.location = path.url
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Inertia.visit(path.url, omit(options, ['openInNewTab']))
|
||||
}
|
||||
}
|
||||
|
||||
applyTheme() {
|
||||
const brandColors = this.config('brandColors')
|
||||
|
||||
if (Object.keys(brandColors).length > 0) {
|
||||
const style = document.createElement('style')
|
||||
|
||||
// Handle converting any non-RGB user strings into valid RGB strings.
|
||||
// This allows the user to specify any color in HSL, RGB, and RGBA
|
||||
// format, and we'll convert it to the proper format for them.
|
||||
let css = Object.keys(brandColors).reduce((carry, v) => {
|
||||
let colorValue = brandColors[v]
|
||||
let validColor = parseColor(colorValue)
|
||||
|
||||
if (validColor) {
|
||||
let parsedColor = parseColor(
|
||||
ColorTranslator.toRGBA(convertColor(validColor))
|
||||
)
|
||||
|
||||
let rgbaString = `${parsedColor.color.join(' ')} / ${
|
||||
parsedColor.alpha
|
||||
}`
|
||||
|
||||
return carry + `\n --colors-primary-${v}: ${rgbaString};`
|
||||
}
|
||||
|
||||
return carry + `\n --colors-primary-${v}: ${colorValue};`
|
||||
}, '')
|
||||
|
||||
style.innerHTML = `:root {${css}\n}`
|
||||
|
||||
document.head.append(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertColor(parsedColor) {
|
||||
let color = fromPairs(
|
||||
Array.from(parsedColor.mode).map((v, i) => {
|
||||
return [v, parsedColor.color[i]]
|
||||
})
|
||||
)
|
||||
|
||||
if (parsedColor.alpha !== undefined) {
|
||||
color.a = parsedColor.alpha
|
||||
}
|
||||
|
||||
return color
|
||||
}
|
||||
43
nova/resources/js/components.js
Normal file
43
nova/resources/js/components.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import camelCase from 'lodash/camelCase'
|
||||
import upperFirst from 'lodash/upperFirst'
|
||||
import CustomError404 from '@/views/CustomError404'
|
||||
import CustomError403 from '@/views/CustomError403'
|
||||
import CustomAppError from '@/views/CustomAppError'
|
||||
import ResourceIndex from '@/views/Index'
|
||||
import ResourceDetail from '@/views/Detail'
|
||||
import Attach from '@/views/Attach'
|
||||
import UpdateAttached from '@/views/UpdateAttached'
|
||||
// import Lens from '@/views/Lens'
|
||||
|
||||
export function registerViews(app) {
|
||||
// Manually register some views...
|
||||
app.component('CustomError403', CustomError403)
|
||||
app.component('CustomError404', CustomError404)
|
||||
app.component('CustomAppError', CustomAppError)
|
||||
app.component('ResourceIndex', ResourceIndex)
|
||||
app.component('ResourceDetail', ResourceDetail)
|
||||
app.component('AttachResource', Attach)
|
||||
app.component('UpdateAttachedResource', UpdateAttached)
|
||||
// app.component('Lens', Lens)
|
||||
|
||||
const requireComponent = require.context(
|
||||
'./components',
|
||||
true,
|
||||
/[A-Z]\w+\.(vue)$/
|
||||
)
|
||||
|
||||
requireComponent.keys().forEach(fileName => {
|
||||
const componentConfig = requireComponent(fileName)
|
||||
|
||||
const componentName = upperFirst(
|
||||
camelCase(
|
||||
fileName
|
||||
.split('/')
|
||||
.pop()
|
||||
.replace(/\.\w+$/, '')
|
||||
)
|
||||
)
|
||||
|
||||
app.component(componentName, componentConfig.default || componentConfig)
|
||||
})
|
||||
}
|
||||
111
nova/resources/js/components/ActionSelector.vue
Normal file
111
nova/resources/js/components/ActionSelector.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<SelectControl
|
||||
v-bind="$attrs"
|
||||
v-if="actionsForSelect.length > 0"
|
||||
ref="actionSelectControl"
|
||||
size="xs"
|
||||
@change="handleSelectionChange"
|
||||
:options="actionsForSelect"
|
||||
dusk="action-select"
|
||||
selected=""
|
||||
:class="{ 'max-w-[6rem]': width === 'auto', 'w-full': width === 'full' }"
|
||||
:aria-label="__('Select Action')"
|
||||
>
|
||||
<option value="" disabled selected>{{ __('Actions') }}</option>
|
||||
</SelectControl>
|
||||
|
||||
<!-- Confirm Action Modal -->
|
||||
<component
|
||||
class="text-left"
|
||||
v-if="actionModalVisible"
|
||||
:show="actionModalVisible"
|
||||
:is="selectedAction?.component"
|
||||
:working="working"
|
||||
:selected-resources="selectedResources"
|
||||
:resource-name="resourceName"
|
||||
:action="selectedAction"
|
||||
:errors="errors"
|
||||
@confirm="executeAction"
|
||||
@close="closeConfirmationModal"
|
||||
/>
|
||||
|
||||
<component
|
||||
v-if="responseModalVisible"
|
||||
:show="responseModalVisible"
|
||||
:is="actionResponseData?.modal"
|
||||
@confirm="closeResponseModal"
|
||||
@close="closeResponseModal"
|
||||
:data="actionResponseData"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useActions } from '@/composables/useActions'
|
||||
import { useStore } from 'vuex'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
// Elements
|
||||
const actionSelectControl = ref(null)
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const emitter = defineEmits(['actionExecuted'])
|
||||
|
||||
const props = defineProps({
|
||||
width: { type: String, default: 'auto' },
|
||||
pivotName: { type: String, default: null },
|
||||
|
||||
resourceName: {},
|
||||
viaResource: {},
|
||||
viaResourceId: {},
|
||||
viaRelationship: {},
|
||||
relationshipType: {},
|
||||
pivotActions: {
|
||||
type: Object,
|
||||
default: () => ({ name: 'Pivot', actions: [] }),
|
||||
},
|
||||
actions: { type: Array, default: [] },
|
||||
selectedResources: { type: [Array, String], default: () => [] },
|
||||
endpoint: { type: String, default: null },
|
||||
triggerDuskAttribute: { type: String, default: null },
|
||||
})
|
||||
|
||||
const {
|
||||
errors,
|
||||
actionModalVisible,
|
||||
responseModalVisible,
|
||||
openConfirmationModal,
|
||||
closeConfirmationModal,
|
||||
closeResponseModal,
|
||||
handleActionClick,
|
||||
selectedAction,
|
||||
setSelectedActionKey,
|
||||
determineActionStrategy,
|
||||
working,
|
||||
executeAction,
|
||||
availableActions,
|
||||
availablePivotActions,
|
||||
actionResponseData,
|
||||
} = useActions(props, emitter, store)
|
||||
|
||||
const handleSelectionChange = event => {
|
||||
setSelectedActionKey(event)
|
||||
determineActionStrategy()
|
||||
|
||||
actionSelectControl.value.resetSelection()
|
||||
}
|
||||
|
||||
const actionsForSelect = computed(() => [
|
||||
...availableActions.value.map(a => ({
|
||||
value: a.uriKey,
|
||||
label: a.name,
|
||||
disabled: a.authorizedToRun === false,
|
||||
})),
|
||||
...availablePivotActions.value.map(a => ({
|
||||
group: props.pivotName,
|
||||
value: a.uriKey,
|
||||
label: a.name,
|
||||
disabled: a.authorizedToRun === false,
|
||||
})),
|
||||
])
|
||||
</script>
|
||||
47
nova/resources/js/components/AppLogo.vue
Normal file
47
nova/resources/js/components/AppLogo.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<PassthroughLogo v-if="logo" :logo="logo" :class="$attrs.class" />
|
||||
<svg
|
||||
v-else
|
||||
:class="$attrs.class"
|
||||
class="h-6"
|
||||
viewBox="0 0 204 37"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<radialGradient
|
||||
cx="-4.619%"
|
||||
cy="6.646%"
|
||||
fx="-4.619%"
|
||||
fy="6.646%"
|
||||
r="101.342%"
|
||||
gradientTransform="matrix(.8299 .53351 -.5579 .79363 .03 .038)"
|
||||
id="a"
|
||||
>
|
||||
<stop stop-color="#00FFC4" offset="0%" />
|
||||
<stop stop-color="#00E1FF" offset="100%" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g fill-rule="nonzero" fill="none">
|
||||
<path
|
||||
d="M30.343 9.99a14.757 14.757 0 0 1 .046 20.972 18.383 18.383 0 0 1-13.019 5.365A18.382 18.382 0 0 1 3.272 29.79c7.209 5.955 17.945 5.581 24.713-1.118a11.477 11.477 0 0 0 0-16.345c-4.56-4.514-11.953-4.514-16.513 0a4.918 4.918 0 0 0 0 7.006 5.04 5.04 0 0 0 7.077 0 1.68 1.68 0 0 1 2.359 0 1.639 1.639 0 0 1 0 2.333 8.4 8.4 0 0 1-11.794 0 8.198 8.198 0 0 1 0-11.674c5.861-5.805 15.366-5.805 21.229 0ZM17.37 0a18.38 18.38 0 0 1 14.097 6.538C24.257.583 13.52.958 6.756 7.653v.002a11.477 11.477 0 0 0 0 16.346c4.558 4.515 11.95 4.515 16.51 0a4.918 4.918 0 0 0 0-7.005 5.04 5.04 0 0 0-7.077 0 1.68 1.68 0 0 1-2.358 0 1.639 1.639 0 0 1 0-2.334 8.4 8.4 0 0 1 11.794 0 8.198 8.198 0 0 1 0 11.674c-5.862 5.805-15.367 5.805-21.23 0a14.756 14.756 0 0 1-.02-20.994A18.383 18.383 0 0 1 17.37 0Z"
|
||||
fill="url(#a)"
|
||||
/>
|
||||
<path
|
||||
d="M59.211 27.49a1.68 1.68 0 0 0 1.69-1.69 1.68 1.68 0 0 0-1.69-1.69h-6.88V12.306c0-1.039-.82-1.86-1.86-1.86-1.037 0-1.858.821-1.858 1.86v13.325c0 1.039.82 1.858 1.859 1.858h8.74Zm9.318-13.084c2.004 0 3.453.531 4.37 1.448.965.967 1.4 2.39 1.4 4.13v5.888c0 .99-.798 1.763-1.787 1.763-1.062 0-1.763-.749-1.763-1.52v-.026c-.893.99-2.123 1.642-3.91 1.642-2.438 0-4.441-1.4-4.441-3.959v-.048c0-2.824 2.148-4.128 5.214-4.128a9.195 9.195 0 0 1 3.163.532v-.218c0-1.521-.944-2.366-2.777-2.366a8.416 8.416 0 0 0-2.535.361 1.525 1.525 0 0 1-.53.098c-.846 0-1.521-.652-1.521-1.496 0-.635.394-1.203.989-1.425 1.16-.435 2.414-.676 4.128-.676Zm-.05 7.387c-1.567 0-2.533.628-2.533 1.786v.047c0 .99.821 1.57 2.005 1.57h-.001l.195-.004c1.541-.066 2.59-.915 2.672-2.113l.005-.151v-.653c-.628-.289-1.448-.482-2.342-.482Zm10.817 5.842c1.014 0 1.833-.82 1.833-1.835v-3.428c0-2.607 1.04-4.03 2.898-4.465.748-.17 1.375-.75 1.375-1.714 0-1.04-.652-1.787-1.785-1.787-1.088 0-1.956 1.159-2.486 2.415v-.58a1.835 1.835 0 1 0-3.67 0v9.56c0 1.013.82 1.833 1.833 1.833l.002.001Zm13.01-13.229c2.005 0 3.453.531 4.37 1.448.965.967 1.4 2.39 1.4 4.13v5.888c0 .99-.797 1.763-1.786 1.763-1.063 0-1.763-.749-1.763-1.52v-.026c-.893.99-2.123 1.643-3.911 1.643-2.438-.001-4.44-1.401-4.44-3.96v-.048c0-2.824 2.148-4.128 5.214-4.128a9.195 9.195 0 0 1 3.162.532v-.218c0-1.521-.943-2.366-2.776-2.366a8.416 8.416 0 0 0-2.535.361 1.525 1.525 0 0 1-.53.098c-.847 0-1.522-.652-1.522-1.496 0-.635.395-1.203.99-1.425 1.16-.435 2.413-.676 4.127-.676Zm-.048 7.387c-1.568 0-2.534.628-2.534 1.786v.047c0 .99.821 1.57 2.003 1.57 1.714 0 2.872-.94 2.872-2.268v-.653c-.627-.289-1.447-.482-2.341-.482Zm14.17 5.963c.99 0 1.667-.653 2.076-1.593l3.959-9.15c.072-.169.194-.555.194-.869a1.736 1.736 0 0 0-1.764-1.738c-.965 0-1.472.628-1.712 1.255l-2.825 7.556-2.775-7.508c-.267-.748-.798-1.303-1.788-1.303-.989 0-1.786.845-1.786 1.714 0 .338.097.652.194.894l3.959 9.149c.41.965 1.086 1.593 2.075 1.593h.194-.001Zm13.977-13.447c4.321 0 6.228 3.55 6.228 6.228 0 1.063-.748 1.763-1.714 1.763h-7.265c.362 1.665 1.52 2.535 3.162 2.535a4.237 4.237 0 0 0 2.607-.87 1.37 1.37 0 0 1 .894-.29c.82 0 1.423.63 1.423 1.449 0 .483-.216.846-.483 1.086-1.134.967-2.607 1.57-4.49 1.57-3.886 0-6.758-2.728-6.758-6.687v-.047c0-3.695 2.63-6.737 6.396-6.737Zm0 2.945c-1.52 0-2.51 1.086-2.8 2.753h5.528c-.217-1.642-1.183-2.753-2.728-2.753Zm11.033 10.381c1.014 0 1.833-.82 1.833-1.835V11.556a1.834 1.834 0 0 0-3.668 0V25.8c0 1.014.82 1.833 1.833 1.833l.002.003Zm14.75 0c1.013 0 1.833-.82 1.833-1.835v-9.053l7.435 9.753c.507.653 1.039 1.086 1.93 1.086h.123c1.037 0 1.858-.82 1.858-1.858V12.283a1.835 1.835 0 0 0-3.67 0v8.713l-7.17-9.415c-.505-.651-1.037-1.086-1.93-1.086h-.386c-1.038 0-1.859.821-1.859 1.859v13.445c0 1.014.82 1.836 1.834 1.836h.001Zm23.244-13.326c4.007 0 6.976 2.97 6.976 6.687v.048c0 3.719-2.993 6.735-7.024 6.735-4.007 0-6.976-2.97-6.976-6.686v-.047c0-3.719 2.993-6.737 7.024-6.737Zm-.048 3.163c-2.1 0-3.355 1.617-3.355 3.524v.048c0 1.907 1.375 3.573 3.403 3.573 2.1 0 3.355-1.617 3.355-3.524v-.049c0-1.905-1.375-3.572-3.403-3.572Zm14.798 10.284c.99 0 1.664-.653 2.076-1.593l3.958-9.15c.072-.169.195-.555.195-.869a1.736 1.736 0 0 0-1.764-1.738c-.966 0-1.473.628-1.713 1.255l-2.825 7.556-2.777-7.508c-.264-.748-.796-1.303-1.786-1.303-.989 0-1.786.845-1.786 1.714 0 .338.097.652.194.894l3.959 9.149c.41.965 1.086 1.593 2.075 1.593h.194Zm13.76-13.35c2.003 0 3.451.531 4.368 1.448.967.967 1.4 2.39 1.4 4.13v5.888c0 .99-.796 1.763-1.786 1.763-1.061 0-1.761-.749-1.761-1.52v-.026c-.894.99-2.126 1.642-3.91 1.642-2.44 0-4.444-1.4-4.444-3.959v-.048c0-2.824 2.149-4.128 5.215-4.128a9.195 9.195 0 0 1 3.162.532v-.218c0-1.521-.942-2.366-2.776-2.366a8.416 8.416 0 0 0-2.535.361 1.52 1.52 0 0 1-.53.098c-.845 0-1.522-.652-1.522-1.496 0-.636.395-1.204.99-1.425 1.159-.435 2.415-.676 4.129-.676Zm-.049 7.387c-1.57 0-2.535.628-2.535 1.786v.047c0 .99.821 1.57 2.004 1.57 1.714 0 2.873-.94 2.873-2.268v-.653c-.628-.289-1.449-.482-2.342-.482Z"
|
||||
class="fill-current text-gray-600 dark:text-white"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
|
||||
computed: {
|
||||
logo() {
|
||||
return window.Nova.config('logo')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
22
nova/resources/js/components/Avatar.vue
Normal file
22
nova/resources/js/components/Avatar.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<img :src="src" :class="avatarClasses" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
src: { type: String },
|
||||
rounded: { type: Boolean, default: true },
|
||||
small: { type: Boolean },
|
||||
medium: { type: Boolean },
|
||||
large: { type: Boolean },
|
||||
})
|
||||
|
||||
const avatarClasses = computed(() => [
|
||||
props.small && 'w-6 h-6',
|
||||
props.medium && !props.small && !props.large && 'w-8 h-8',
|
||||
props.large && 'w-12 h-12',
|
||||
props.rounded && 'rounded-full',
|
||||
])
|
||||
</script>
|
||||
40
nova/resources/js/components/Backdrop.vue
Normal file
40
nova/resources/js/components/Backdrop.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
v-show="props.show"
|
||||
class="absolute inset-0 h-full"
|
||||
:style="{ top: `${scrollY}px` }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const scrollY = ref()
|
||||
const scrollEvent = () => {
|
||||
scrollY.value = window.scrollY
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollEvent()
|
||||
|
||||
document.addEventListener('scroll', scrollEvent)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('scroll', scrollEvent)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
27
nova/resources/js/components/Badges/Badge.vue
Normal file
27
nova/resources/js/components/Badges/Badge.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<span
|
||||
class="inline-flex items-center whitespace-nowrap min-h-6 px-2 rounded-full uppercase text-xs font-bold"
|
||||
:class="extraClasses"
|
||||
>
|
||||
<slot name="icon" />
|
||||
<slot>
|
||||
{{ label }}
|
||||
</slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: [Boolean, String],
|
||||
required: false,
|
||||
},
|
||||
|
||||
extraClasses: {
|
||||
type: [Array, String],
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
13
nova/resources/js/components/Badges/CircleBadge.vue
Normal file
13
nova/resources/js/components/Badges/CircleBadge.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<span
|
||||
class="h-4 inline-flex items-center justify-center font-bold rounded-full px-2 text-mono text-xs ml-1 bg-primary-100 text-primary-800 dark:bg-primary-500 dark:text-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
//
|
||||
}
|
||||
</script>
|
||||
54
nova/resources/js/components/BooleanOption.vue
Normal file
54
nova/resources/js/components/BooleanOption.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<CheckboxWithLabel
|
||||
:dusk="`${option.value}-checkbox`"
|
||||
:checked="isChecked"
|
||||
@input="updateCheckedState(option.value, $event.target.checked)"
|
||||
>
|
||||
<span>{{ labelFor(option) }}</span>
|
||||
</CheckboxWithLabel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['change'],
|
||||
|
||||
props: {
|
||||
resourceName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
filter: Object,
|
||||
option: Object,
|
||||
label: { default: 'name' },
|
||||
},
|
||||
|
||||
methods: {
|
||||
labelFor(option) {
|
||||
return option[this.label] || ''
|
||||
},
|
||||
|
||||
updateCheckedState(optionKey, checked) {
|
||||
let oldValue = this.filter.currentValue
|
||||
let newValue = { ...oldValue, [optionKey]: checked }
|
||||
|
||||
this.$store.commit(`${this.resourceName}/updateFilterState`, {
|
||||
filterClass: this.filter.class,
|
||||
value: newValue,
|
||||
})
|
||||
|
||||
this.$emit('change')
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isChecked() {
|
||||
return (
|
||||
this.$store.getters[`${this.resourceName}/filterOptionValue`](
|
||||
this.filter.class,
|
||||
this.option.value
|
||||
) == true
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
45
nova/resources/js/components/Buttons/BasicButton.vue
Normal file
45
nova/resources/js/components/Buttons/BasicButton.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<component
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
:is="component"
|
||||
ref="button"
|
||||
class="cursor-pointer rounded text-sm font-bold focus:outline-none focus:ring ring-primary-200 dark:ring-gray-600"
|
||||
:class="{
|
||||
'inline-flex items-center justify-center': align == 'center',
|
||||
'inline-flex items-center justify-start': align == 'left',
|
||||
'h-9 px-3': size == 'lg',
|
||||
'h-8 px-3': size == 'sm',
|
||||
'h-7 px-1 md:px-3': size == 'xs',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'lg',
|
||||
},
|
||||
|
||||
align: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
validator: v => ['left', 'center'].includes(v),
|
||||
},
|
||||
|
||||
component: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
})
|
||||
|
||||
const button = ref(null)
|
||||
|
||||
const focus = () => button.value.focus()
|
||||
|
||||
defineExpose({ focus })
|
||||
</script>
|
||||
24
nova/resources/js/components/Buttons/ButtonInertiaLink.vue
Normal file
24
nova/resources/js/components/Buttons/ButtonInertiaLink.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<Link
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
class="shadow rounded focus:outline-none ring-primary-200 dark:ring-gray-600 focus:ring bg-primary-500 hover:bg-primary-400 active:bg-primary-600 text-white dark:text-gray-800 inline-flex items-center font-bold"
|
||||
:class="{
|
||||
'px-4 h-9 text-sm': size === 'md',
|
||||
'px-3 h-7 text-xs': size === 'sm',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator: val => ['sm', 'md'].includes(val),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
38
nova/resources/js/components/Buttons/CopyButton.vue
Normal file
38
nova/resources/js/components/Buttons/CopyButton.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleClick"
|
||||
class="inline-flex items-center px-2 space-x-1 -mx-2 text-gray-500 dark:text-gray-400 hover:bg-gray-100 hover:text-gray-500 active:text-gray-600 dark:hover:bg-gray-900"
|
||||
:class="{
|
||||
'rounded-lg': !rounded,
|
||||
'rounded-full': rounded,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<CopyIcon v-if="withIcon" :copied="copied" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
const copied = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
rounded: { type: Boolean, default: true },
|
||||
withIcon: { type: Boolean, default: true },
|
||||
})
|
||||
|
||||
const denouncedHandleClick = debounce(
|
||||
() => {
|
||||
copied.value = !copied.value
|
||||
setTimeout(() => (copied.value = !copied.value), 2000)
|
||||
},
|
||||
2000,
|
||||
{ leading: true, trailing: false }
|
||||
)
|
||||
|
||||
const handleClick = () => denouncedHandleClick()
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<Button variant="link" size="small" leading-icon="plus-circle" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Button } from 'laravel-nova-ui'
|
||||
</script>
|
||||
38
nova/resources/js/components/Buttons/DefaultButton.vue
Normal file
38
nova/resources/js/components/Buttons/DefaultButton.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<BasicButton
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
:component="component"
|
||||
ref="button"
|
||||
class="shadow relative bg-primary-500 hover:bg-primary-400 text-white dark:text-gray-900"
|
||||
>
|
||||
<slot />
|
||||
</BasicButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: 'lg',
|
||||
},
|
||||
|
||||
align: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
validator: v => ['left', 'center'].includes(v),
|
||||
},
|
||||
|
||||
component: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.button.focus()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
27
nova/resources/js/components/Buttons/IconButton.vue
Normal file
27
nova/resources/js/components/Buttons/IconButton.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center focus:ring focus:ring-primary-200 focus:outline-none rounded"
|
||||
:class="buttonClasses"
|
||||
>
|
||||
<Icon :type="iconType" class="hover:opacity-50" v-bind="{ solid: solid }" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
iconType: { type: String, default: 'dots-horizontal' },
|
||||
small: { type: Boolean },
|
||||
medium: { type: Boolean },
|
||||
large: { type: Boolean },
|
||||
solid: { type: Boolean, default: true },
|
||||
})
|
||||
|
||||
const buttonClasses = computed(() => [
|
||||
props.small && 'w-6 h-6',
|
||||
props.medium && 'w-8 h-8',
|
||||
props.large && 'w-9 h-9',
|
||||
])
|
||||
</script>
|
||||
19
nova/resources/js/components/Buttons/InertiaButton.vue
Normal file
19
nova/resources/js/components/Buttons/InertiaButton.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import { Button } from 'laravel-nova-ui'
|
||||
|
||||
const props = defineProps({
|
||||
href: { type: String, required: true },
|
||||
variant: { type: String, default: 'primary' },
|
||||
icon: { type: String, default: 'primary' },
|
||||
dusk: { type: String, default: null },
|
||||
label: { type: String, default: null },
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link :href="href" :dusk="dusk">
|
||||
<Button as="div" :variant="variant" :icon="icon" :label="label">
|
||||
<slot />
|
||||
</Button>
|
||||
</Link>
|
||||
</template>
|
||||
17
nova/resources/js/components/Buttons/InvertedButton.vue
Normal file
17
nova/resources/js/components/Buttons/InvertedButton.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="space-x-1 cursor-pointer focus:outline-none focus:ring ring-primary-200 dark:ring-gray-600 focus:ring-offset-4 dark:focus:ring-offset-gray-800 rounded-lg mx-auto text-primary-500 font-bold link-default px-3 rounded-b-lg flex items-center"
|
||||
>
|
||||
<Icon :type="iconType" />
|
||||
<span>
|
||||
<slot />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
iconType: { type: String, default: 'plus-circle' },
|
||||
})
|
||||
</script>
|
||||
29
nova/resources/js/components/Buttons/LinkButton.vue
Normal file
29
nova/resources/js/components/Buttons/LinkButton.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<BasicButton
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
:component="component"
|
||||
class="appearance-none bg-transparent font-bold text-gray-400 hover:text-gray-300 active:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 dark:active:text-gray-600 dark:hover:bg-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</BasicButton>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'lg',
|
||||
},
|
||||
|
||||
align: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
validator: v => ['left', 'center'].includes(v),
|
||||
},
|
||||
|
||||
component: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
15
nova/resources/js/components/Buttons/OutlineButton.vue
Normal file
15
nova/resources/js/components/Buttons/OutlineButton.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<BasicButton
|
||||
v-bind="$attrs"
|
||||
component="button"
|
||||
class="focus:outline-none focus:ring rounded border-2 border-primary-300 dark:border-gray-500 hover:border-primary-500 active:border-primary-400 dark:hover:border-gray-400 dark:active:border-gray-300 bg-white dark:bg-transparent text-primary-500 dark:text-gray-400 px-3 h-9 inline-flex items-center font-bold"
|
||||
>
|
||||
<slot />
|
||||
</BasicButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
//
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<Link
|
||||
v-bind="{ ...$props, ...$attrs }"
|
||||
class="focus:outline-none ring-primary-200 dark:ring-gray-600 focus:ring-2 rounded border-2 border-gray-200 dark:border-gray-500 hover:border-primary-500 active:border-primary-400 dark:hover:border-gray-400 dark:active:border-gray-300 bg-white dark:bg-transparent text-primary-500 dark:text-gray-400 px-3 h-9 inline-flex items-center font-bold"
|
||||
>
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
//
|
||||
}
|
||||
</script>
|
||||
16
nova/resources/js/components/Buttons/RemoveButton.vue
Normal file
16
nova/resources/js/components/Buttons/RemoveButton.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full shadow bg-white dark:bg-gray-800 text-center flex items-center justify-center h-[20px] w-[21px]"
|
||||
>
|
||||
<Icon
|
||||
type="x-circle"
|
||||
:solid="true"
|
||||
class="text-gray-800 dark:text-gray-200"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<button class="px-2" @click="togglePolling" v-tooltip.click="buttonLabel">
|
||||
<svg
|
||||
class="w-6 h-6"
|
||||
:class="{
|
||||
'text-green-500': currentlyPolling,
|
||||
'text-gray-300 dark:text-gray-500': !currentlyPolling,
|
||||
}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
emits: ['start-polling', 'stop-polling'],
|
||||
|
||||
props: {
|
||||
currentlyPolling: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
togglePolling() {
|
||||
return this.currentlyPolling
|
||||
? this.$emit('stop-polling')
|
||||
: this.$emit('start-polling')
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
buttonLabel() {
|
||||
return this.currentlyPolling
|
||||
? this.__('Stop Polling')
|
||||
: this.__('Start Polling')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
20
nova/resources/js/components/Buttons/ToolbarButton.vue
Normal file
20
nova/resources/js/components/Buttons/ToolbarButton.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center w-8 h-8 focus:outline-none focus:ring ring-primary-200 dark:ring-gray-600 rounded-lg"
|
||||
>
|
||||
<slot />
|
||||
<Icon v-if="type" solid :type="type" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
33
nova/resources/js/components/CancelButton.vue
Normal file
33
nova/resources/js/components/CancelButton.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<LinkButton
|
||||
v-bind="{ size, align, ...$props, ...$attrs }"
|
||||
type="button"
|
||||
:component="component"
|
||||
>
|
||||
<slot>
|
||||
{{ __('Cancel') }}
|
||||
</slot>
|
||||
</LinkButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: 'lg',
|
||||
},
|
||||
|
||||
align: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
validator: v => ['left', 'center'].includes(v),
|
||||
},
|
||||
|
||||
component: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
13
nova/resources/js/components/Card.vue
Normal file
13
nova/resources/js/components/Card.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative overflow-hidden bg-white dark:bg-gray-800 rounded-lg shadow"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
//
|
||||
}
|
||||
</script>
|
||||
64
nova/resources/js/components/CardWrapper.vue
Normal file
64
nova/resources/js/components/CardWrapper.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<component
|
||||
:class="[widthClass, heightClass]"
|
||||
:key="`${card.component}.${card.uriKey}`"
|
||||
class="h-full"
|
||||
:is="card.component"
|
||||
:card="card"
|
||||
:resource="resource"
|
||||
:resourceName="resourceName"
|
||||
:resourceId="resourceId"
|
||||
:lens="lens"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
card: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
resource: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
|
||||
resourceName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
resourceId: {
|
||||
type: [Number, String],
|
||||
default: '',
|
||||
},
|
||||
|
||||
lens: {
|
||||
lens: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* The class given to the card wrappers based on its width
|
||||
*/
|
||||
widthClass() {
|
||||
return {
|
||||
full: 'md:col-span-12',
|
||||
'1/3': 'md:col-span-4',
|
||||
'1/2': 'md:col-span-6',
|
||||
'1/4': 'md:col-span-3',
|
||||
'2/3': 'md:col-span-8',
|
||||
'3/4': 'md:col-span-9',
|
||||
}[this.card.width]
|
||||
},
|
||||
|
||||
heightClass() {
|
||||
return this.card.height == 'fixed' ? 'min-h-40' : ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user