This commit is contained in:
2024-09-01 18:54:23 +05:00
parent 76d18365a5
commit 061f09eca1
1597 changed files with 109451 additions and 1 deletions

View 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

File diff suppressed because it is too large Load Diff

158
nova/resources/css/form.css Normal file
View 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
View 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;
}

View File

@@ -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]'
)
})

View 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)
})

View File

@@ -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')
})

View 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'
)
})

View 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)
})

View 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)
})

View 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('!')
})

View 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',
})
)
})

View 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'
)
})

View 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
View 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
}

View 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)
})
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,7 @@
<template>
<Button variant="link" size="small" leading-icon="plus-circle" />
</template>
<script setup>
import { Button } from 'laravel-nova-ui'
</script>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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