baseline
BIN
web/.DS_Store
vendored
Normal file
9
web/.htaccess
Normal file
@@ -0,0 +1,9 @@
|
||||
php_flag display_startup_errors on
|
||||
php_flag display_errors on
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||
</IfModule>
|
||||
35
web/brand/logo.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 146.51 121.08">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #71ab51;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #1e7729;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #3d8c33;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #c4d6a4;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #69af67;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="cls-4" d="M123.06,30.78L103.94,3.63c-1.41-2.01-4.54-3.63-6.99-3.63h-47.39c-1.18,0-2.53.39-3.76,1.01l20.06,29.78h57.2Z"/>
|
||||
<path class="cls-2" d="M90.2,64.39l-39.69,56.69h91.56c2.45,0,4.44-1.99,4.44-4.44v-47.82c0-1.29-.32-2.9-.83-4.44h-55.49Z"/>
|
||||
<path class="cls-1" d="M45.8,1.01c-1.31.66-2.5,1.59-3.23,2.62L2.55,60.47c-1.41,2-2.55,5.62-2.55,8.07v48.1c0,1.12.43,2.13,1.11,2.91L65.86,30.78,45.8,1.01Z"/>
|
||||
<path class="cls-3" d="M65.86,30.78L1.11,119.56c.81.93,1.99,1.53,3.33,1.53h46.07l39.69-56.69-24.34-33.61Z"/>
|
||||
<path class="cls-5" d="M90.2,64.39h55.49c-.45-1.37-1.05-2.69-1.71-3.64l-20.92-29.97h-57.2l24.34,33.61Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
713
web/css/base/variables.css
Normal file
@@ -0,0 +1,713 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
:host,
|
||||
:root {
|
||||
--app-font-family-emoji:
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--app-font-family-sans-serif:
|
||||
system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial,
|
||||
"Helvetica Neue", sans-serif, var(--app-font-family-emoji);
|
||||
--app-font-family-monospace:
|
||||
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono",
|
||||
monospace, var(--app-font-family-emoji);
|
||||
--app-font-family: var(--app-font-family-sans-serif);
|
||||
--app-line-height: 1.5;
|
||||
--app-font-weight: 400;
|
||||
--app-font-size: 100%;
|
||||
--app-text-underline-offset: 0.1rem;
|
||||
--app-border-radius: 0.5rem;
|
||||
--app-border-width: 0.0625rem;
|
||||
--app-outline-width: 0.125rem;
|
||||
--app-transition: 0.2s ease-in-out;
|
||||
--app-spacing: 1rem;
|
||||
--app-typography-spacing-vertical: 1rem;
|
||||
--app-block-spacing-vertical: 1.5rem;
|
||||
--app-block-spacing-horizontal: 1.5rem;
|
||||
--app-grid-column-gap: var(--app-spacing);
|
||||
--app-grid-row-gap: var(--app-spacing);
|
||||
--app-flex-gap: var(--app-spacing);
|
||||
--app-form-element-spacing-vertical: .5rem;
|
||||
--app-form-element-spacing-horizontal: .7rem;
|
||||
--app-button-padding-vertical: var(--app-form-element-spacing-vertical);
|
||||
--app-button-padding-horizontal: var(--app-form-element-spacing-horizontal);
|
||||
--app-button-padding-vertical-small: calc(
|
||||
var(--app-form-element-spacing-vertical) * 0.3
|
||||
);
|
||||
--app-button-padding-horizontal-small: calc(
|
||||
var(--app-form-element-spacing-horizontal) * 0.5
|
||||
);
|
||||
--app-group-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-group-box-shadow-focus-with-button: 0 0 0 var(--app-outline-width)
|
||||
var(--app-primary-focus);
|
||||
--app-group-box-shadow-focus-with-input: 0 0 0 0.0625rem
|
||||
var(--app-form-element-border-color);
|
||||
--app-modal-overlay-backdrop-filter: blur(0.375rem);
|
||||
--app-nav-element-spacing-vertical: 1rem;
|
||||
--app-nav-element-spacing-horizontal: 0.5rem;
|
||||
--app-nav-link-spacing-vertical: 0.5rem;
|
||||
--app-nav-link-spacing-horizontal: 0.5rem;
|
||||
--app-nav-breadcrumb-divider: ">";
|
||||
--app-primary-h-base: 180.75;
|
||||
--app-primary-s-base: 37.38%;
|
||||
--app-primary-l-base: 58.04%;
|
||||
--app-secondary-h-base: 223;
|
||||
--app-secondary-s-base: 19%;
|
||||
--app-secondary-l-base: 45%;
|
||||
--app-badge-background-color: coral;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
:host,
|
||||
:root {
|
||||
--app-icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
--app-icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");
|
||||
--app-icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
--app-icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");
|
||||
--app-icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
--app-icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
|
||||
--app-icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
|
||||
--app-icon-loading: url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
:host,
|
||||
:root {
|
||||
--app-font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:host,
|
||||
:root {
|
||||
--app-font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
:host,
|
||||
:root {
|
||||
--app-font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
:host,
|
||||
:root {
|
||||
--app-font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
:host,
|
||||
:root {
|
||||
--app-font-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:host(:not([data-theme="dark"])),
|
||||
:root:not([data-theme="dark"]),
|
||||
[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--app-background-color: #fff;
|
||||
--app-sidebar-background-color: rgb(247 250 252);
|
||||
--app-color: #373c44;
|
||||
--app-text-selection-color: rgba(2, 154, 232, 0.25);
|
||||
--app-muted-color: #646b79;
|
||||
--app-muted-border-color: rgb(231, 234, 239.5);
|
||||
--app-primary-h-light: 180.75;
|
||||
--app-primary-s-light: 37.38%;
|
||||
--app-primary-l-light: 58.04%;
|
||||
--app-primary-h: var(--app-primary-h-light, var(--app-primary-h-base));
|
||||
--app-primary-s: var(--app-primary-s-light, var(--app-primary-s-base));
|
||||
--app-primary-l: var(--app-primary-l-light, var(--app-primary-l-base));
|
||||
--app-primary-l-hover: -7%;
|
||||
--app-primary-l-hover-bg: -4%;
|
||||
--app-primary-l-focus: 16%;
|
||||
--app-primary: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s) var(--app-primary-l)
|
||||
);
|
||||
--app-primary-background: var(--app-primary);
|
||||
--app-primary-border: var(--app-primary-background);
|
||||
--app-primary-underline: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s) var(--app-primary-l) / 0.5
|
||||
);
|
||||
--app-primary-hover: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-hover))
|
||||
);
|
||||
--app-primary-hover-background: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-hover-bg))
|
||||
);
|
||||
--app-primary-hover-border: var(--app-primary-hover-background);
|
||||
--app-primary-hover-underline: var(--app-primary-hover);
|
||||
--app-primary-focus: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-focus)) / 0.5
|
||||
);
|
||||
--app-primary-inverse: #fff;
|
||||
--app-secondary-h-light: 223;
|
||||
--app-secondary-s-light: 19%;
|
||||
--app-secondary-l-light: 45%;
|
||||
--app-secondary-h: var(--app-secondary-h-light, var(--app-secondary-h-base));
|
||||
--app-secondary-s: var(--app-secondary-s-light, var(--app-secondary-s-base));
|
||||
--app-secondary-l: var(--app-secondary-l-light, var(--app-secondary-l-base));
|
||||
--app-secondary-l-hover: -6%;
|
||||
--app-secondary-l-hover-bg: -6%;
|
||||
--app-secondary-l-focus: 0%;
|
||||
--app-secondary: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l)
|
||||
);
|
||||
--app-secondary-background: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) - 4%)
|
||||
);
|
||||
--app-secondary-border: var(--app-secondary-background);
|
||||
--app-secondary-underline: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l) / 0.5
|
||||
);
|
||||
--app-secondary-hover: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) + var(--app-secondary-l-hover))
|
||||
);
|
||||
--app-secondary-hover-background: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) + var(--app-secondary-l-hover-bg))
|
||||
);
|
||||
--app-secondary-hover-border: var(--app-secondary-hover-background);
|
||||
--app-secondary-hover-underline: var(--app-secondary-hover);
|
||||
--app-secondary-focus: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l) / 0.25
|
||||
);
|
||||
--app-secondary-inverse: #fff;
|
||||
--app-contrast: #181c25;
|
||||
--app-contrast-background: #181c25;
|
||||
--app-contrast-border: var(--app-contrast-background);
|
||||
--app-contrast-underline: rgba(24, 28, 37, 0.5);
|
||||
--app-contrast-hover: #000;
|
||||
--app-contrast-hover-background: #000;
|
||||
--app-contrast-hover-border: var(--app-contrast-hover-background);
|
||||
--app-contrast-hover-underline: var(--app-secondary-hover);
|
||||
--app-contrast-focus: rgba(93, 107, 137, 0.25);
|
||||
--app-contrast-inverse: #fff;
|
||||
--app-box-shadow:
|
||||
0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),
|
||||
0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),
|
||||
0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),
|
||||
0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),
|
||||
0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),
|
||||
0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),
|
||||
0 0 0 0.0625rem rgba(129, 145, 181, 0.015);
|
||||
--app-h1-color: #2d3138;
|
||||
--app-h2-color: #373c44;
|
||||
--app-h3-color: #424751;
|
||||
--app-h4-color: #4d535e;
|
||||
--app-h5-color: #5c6370;
|
||||
--app-h6-color: #646b79;
|
||||
--app-mark-background-color: rgb(252.5, 230.5, 191.5);
|
||||
--app-mark-color: #0f1114;
|
||||
--app-ins-color: rgb(28.5, 105.5, 84);
|
||||
--app-del-color: rgb(136, 56.5, 53);
|
||||
--app-blockquote-border-color: var(--app-muted-border-color);
|
||||
--app-blockquote-footer-color: var(--app-muted-color);
|
||||
--app-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-table-border-color: var(--app-muted-border-color);
|
||||
--app-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375);
|
||||
--app-code-background-color: rgb(243, 244.5, 246.75);
|
||||
--app-code-color: #646b79;
|
||||
--app-code-kbd-background-color: var(--app-color);
|
||||
--app-code-kbd-color: var(--app-background-color);
|
||||
--app-form-element-background-color: rgb(251, 251.5, 252.25);
|
||||
--app-form-element-selected-background-color: #dfe3eb;
|
||||
--app-form-element-border-color: #cfd5e2;
|
||||
--app-border: var(--app-form-element-border-color);
|
||||
--app-form-element-color: #23262c;
|
||||
--app-form-element-placeholder-color: var(--app-muted-color);
|
||||
--app-form-element-active-background-color: #fff;
|
||||
--app-form-element-active-border-color: var(--app-primary-border);
|
||||
--app-form-element-focus-color: var(--app-primary-border);
|
||||
--app-form-element-disabled-opacity: 0.5;
|
||||
--app-form-element-invalid-border-color: rgb(183.5, 105.5, 106.5);
|
||||
--app-form-element-invalid-active-border-color: rgb(200.25, 79.25, 72.25);
|
||||
--app-form-element-invalid-focus-color: var(
|
||||
--app-form-element-invalid-active-border-color
|
||||
);
|
||||
--app-form-element-valid-border-color: rgb(76, 154.5, 137.5);
|
||||
--app-form-element-valid-active-border-color: rgb(39, 152.75, 118.75);
|
||||
--app-form-element-valid-focus-color: var(
|
||||
--app-form-element-valid-active-border-color
|
||||
);
|
||||
--app-notice-success-color: var(--app-form-element-valid-border-color);
|
||||
--app-notice-success-border-color: var(--app-form-element-valid-border-color);
|
||||
--app-notice-success-background-color: rgba(76, 154.5, 137.5, 0.12);
|
||||
--app-notice-warning-color: rgb(173, 113, 34);
|
||||
--app-notice-warning-border-color: rgb(214, 145, 55);
|
||||
--app-notice-warning-background-color: rgba(214, 145, 55, 0.12);
|
||||
--app-notice-info-color: var(--app-secondary);
|
||||
--app-notice-info-border-color: var(--app-secondary);
|
||||
--app-notice-info-background-color: rgba(96.9, 110.5, 137.7, 0.12);
|
||||
--app-notice-error-color: var(--app-form-element-invalid-border-color);
|
||||
--app-notice-error-border-color: var(--app-form-element-invalid-border-color);
|
||||
--app-notice-error-background-color: rgb(255 0 0 / 13%);
|
||||
--app-switch-background-color: #bfc7d9;
|
||||
--app-switch-checked-background-color: var(--app-primary-background);
|
||||
--app-switch-color: #fff;
|
||||
--app-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-range-border-color: #dfe3eb;
|
||||
--app-range-active-border-color: #bfc7d9;
|
||||
--app-range-thumb-border-color: var(--app-background-color);
|
||||
--app-range-thumb-color: var(--app-secondary-background);
|
||||
--app-range-thumb-active-color: var(--app-primary-background);
|
||||
--app-accordion-border-color: var(--app-muted-border-color);
|
||||
--app-accordion-active-summary-color: var(--app-primary-hover);
|
||||
--app-accordion-close-summary-color: var(--app-color);
|
||||
--app-accordion-open-summary-color: var(--app-muted-color);
|
||||
--app-card-background-color: var(--app-background-color);
|
||||
--app-card-border-color: var(--app-muted-border-color);
|
||||
--app-card-box-shadow: var(--app-box-shadow);
|
||||
--app-card-sectioning-background-color: rgb(251, 251.5, 252.25);
|
||||
--app-dropdown-background-color: #fff;
|
||||
--app-dropdown-border-color: #eff1f4;
|
||||
--app-dropdown-box-shadow: var(--app-box-shadow);
|
||||
--app-dropdown-color: var(--app-color);
|
||||
--app-dropdown-hover-background-color: #eff1f4;
|
||||
--app-loading-spinner-opacity: 0.5;
|
||||
--app-modal-overlay-background-color: rgba(232, 234, 237, 0.75);
|
||||
--app-progress-background-color: #dfe3eb;
|
||||
--app-progress-color: var(--app-primary-background);
|
||||
--app-tooltip-background-color: var(--app-contrast-background);
|
||||
--app-tooltip-color: var(--app-contrast-inverse);
|
||||
--app-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 154.5, 137.5)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
--app-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200.25, 79.25, 72.25)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
:host(:not([data-theme="dark"]))
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="checkbox"],
|
||||
[type="radio"],
|
||||
[type="file"]
|
||||
),
|
||||
:root:not([data-theme="dark"])
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="checkbox"],
|
||||
[type="radio"],
|
||||
[type="file"]
|
||||
),
|
||||
[data-theme="light"]
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="checkbox"],
|
||||
[type="radio"],
|
||||
[type="file"]
|
||||
) {
|
||||
--app-form-element-focus-color: var(--app-primary-focus);
|
||||
}
|
||||
|
||||
@media only screen and (prefers-color-scheme: dark) {
|
||||
:host(:not([data-theme])),
|
||||
:root:not([data-theme]) {
|
||||
color-scheme: dark;
|
||||
--app-background-color: rgb(19, 22.5, 30.5);
|
||||
--app-sidebar-background-color: rgb(18 20 27);
|
||||
--app-color: #c2c7d0;
|
||||
--app-text-selection-color: rgba(1, 170, 255, 0.1875);
|
||||
--app-muted-color: #7b8495;
|
||||
--app-muted-border-color: #202632;
|
||||
--app-primary-h-dark: var(--app-primary-h-light, var(--app-primary-h-base));
|
||||
--app-primary-s-dark: var(--app-primary-s-light, var(--app-primary-s-base));
|
||||
--app-primary-l-dark: var(--app-primary-l-light, var(--app-primary-l-base));
|
||||
--app-primary-h: var(--app-primary-h-dark, var(--app-primary-h-base));
|
||||
--app-primary-s: var(--app-primary-s-dark, var(--app-primary-s-base));
|
||||
--app-primary-l: var(--app-primary-l-dark, var(--app-primary-l-base));
|
||||
--app-primary-l-hover: -7%;
|
||||
--app-primary-l-hover-bg: -4%;
|
||||
--app-primary-l-focus: 16%;
|
||||
--app-primary: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s) var(--app-primary-l)
|
||||
);
|
||||
--app-primary-background: var(--app-primary);
|
||||
--app-primary-border: var(--app-primary-background);
|
||||
--app-primary-underline: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s) var(--app-primary-l) / 0.5
|
||||
);
|
||||
--app-primary-hover: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-hover))
|
||||
);
|
||||
--app-primary-hover-background: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-hover-bg))
|
||||
);
|
||||
--app-primary-hover-border: var(--app-primary-hover-background);
|
||||
--app-primary-hover-underline: var(--app-primary-hover);
|
||||
--app-primary-focus: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-focus)) / 0.5
|
||||
);
|
||||
--app-primary-inverse: #fff;
|
||||
--app-secondary-h-dark: var(
|
||||
--app-secondary-h-light,
|
||||
var(--app-secondary-h-base)
|
||||
);
|
||||
--app-secondary-s-dark: var(
|
||||
--app-secondary-s-light,
|
||||
var(--app-secondary-s-base)
|
||||
);
|
||||
--app-secondary-l-dark: var(
|
||||
--app-secondary-l-light,
|
||||
var(--app-secondary-l-base)
|
||||
);
|
||||
--app-secondary-h: var(--app-secondary-h-dark, var(--app-secondary-h-base));
|
||||
--app-secondary-s: var(--app-secondary-s-dark, var(--app-secondary-s-base));
|
||||
--app-secondary-l: var(--app-secondary-l-dark, var(--app-secondary-l-base));
|
||||
--app-secondary-l-hover: -6%;
|
||||
--app-secondary-l-hover-bg: -6%;
|
||||
--app-secondary-l-focus: 0%;
|
||||
--app-secondary: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l)
|
||||
);
|
||||
--app-secondary-background: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) - 4%)
|
||||
);
|
||||
--app-secondary-border: var(--app-secondary-background);
|
||||
--app-secondary-underline: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l) / 0.5
|
||||
);
|
||||
--app-secondary-hover: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) + var(--app-secondary-l-hover))
|
||||
);
|
||||
--app-secondary-hover-background: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) + var(--app-secondary-l-hover-bg))
|
||||
);
|
||||
--app-secondary-hover-border: var(--app-secondary-hover-background);
|
||||
--app-secondary-hover-underline: var(--app-secondary-hover);
|
||||
--app-secondary-focus: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l) /
|
||||
0.25
|
||||
);
|
||||
--app-secondary-inverse: #fff;
|
||||
--app-contrast: #dfe3eb;
|
||||
--app-contrast-background: #eff1f4;
|
||||
--app-contrast-border: var(--app-contrast-background);
|
||||
--app-contrast-underline: rgba(223, 227, 235, 0.5);
|
||||
--app-contrast-hover: #fff;
|
||||
--app-contrast-hover-background: #fff;
|
||||
--app-contrast-hover-border: var(--app-contrast-hover-background);
|
||||
--app-contrast-hover-underline: var(--app-contrast-hover);
|
||||
--app-contrast-focus: rgba(207, 213, 226, 0.25);
|
||||
--app-contrast-inverse: #000;
|
||||
--app-box-shadow:
|
||||
0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),
|
||||
0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),
|
||||
0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),
|
||||
0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),
|
||||
0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),
|
||||
0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),
|
||||
0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);
|
||||
--app-h1-color: #f0f1f3;
|
||||
--app-h2-color: #e0e3e7;
|
||||
--app-h3-color: #c2c7d0;
|
||||
--app-h4-color: #b3b9c5;
|
||||
--app-h5-color: #a4acba;
|
||||
--app-h6-color: #8891a4;
|
||||
--app-mark-background-color: #014063;
|
||||
--app-mark-color: #fff;
|
||||
--app-ins-color: #62af9a;
|
||||
--app-del-color: rgb(205.5, 126, 123);
|
||||
--app-blockquote-border-color: var(--app-muted-border-color);
|
||||
--app-blockquote-footer-color: var(--app-muted-color);
|
||||
--app-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-table-border-color: var(--app-muted-border-color);
|
||||
--app-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375);
|
||||
--app-code-background-color: rgb(26, 30.5, 40.25);
|
||||
--app-code-color: #8891a4;
|
||||
--app-code-kbd-background-color: var(--app-color);
|
||||
--app-code-kbd-color: var(--app-background-color);
|
||||
--app-form-element-background-color: rgb(28, 33, 43.5);
|
||||
--app-form-element-selected-background-color: #2a3140;
|
||||
--app-form-element-border-color: #2a3140;
|
||||
--app-border: var(--app-form-element-border-color);
|
||||
--app-form-element-color: #e0e3e7;
|
||||
--app-form-element-placeholder-color: #8891a4;
|
||||
--app-form-element-active-background-color: rgb(26, 30.5, 40.25);
|
||||
--app-form-element-active-border-color: var(--app-primary-border);
|
||||
--app-form-element-focus-color: var(--app-primary-border);
|
||||
--app-form-element-disabled-opacity: 0.5;
|
||||
--app-form-element-invalid-border-color: rgb(149.5, 74, 80);
|
||||
--app-form-element-invalid-active-border-color: rgb(183.25, 63.5, 59);
|
||||
--app-form-element-invalid-focus-color: var(
|
||||
--app-form-element-invalid-active-border-color
|
||||
);
|
||||
--app-form-element-valid-border-color: #2a7b6f;
|
||||
--app-form-element-valid-active-border-color: rgb(22, 137, 105.5);
|
||||
--app-form-element-valid-focus-color: var(
|
||||
--app-form-element-valid-active-border-color
|
||||
);
|
||||
--app-switch-background-color: #333c4e;
|
||||
--app-switch-checked-background-color: var(--app-primary-background);
|
||||
--app-switch-color: #fff;
|
||||
--app-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-range-border-color: #202632;
|
||||
--app-range-active-border-color: #2a3140;
|
||||
--app-range-thumb-border-color: var(--app-background-color);
|
||||
--app-range-thumb-color: var(--app-secondary-background);
|
||||
--app-range-thumb-active-color: var(--app-primary-background);
|
||||
--app-accordion-border-color: var(--app-muted-border-color);
|
||||
--app-accordion-active-summary-color: var(--app-primary-hover);
|
||||
--app-accordion-close-summary-color: var(--app-color);
|
||||
--app-accordion-open-summary-color: var(--app-muted-color);
|
||||
--app-card-background-color: #181c25;
|
||||
--app-card-border-color: var(--app-card-background-color);
|
||||
--app-card-box-shadow: var(--app-box-shadow);
|
||||
--app-card-sectioning-background-color: rgb(26, 30.5, 40.25);
|
||||
--app-dropdown-background-color: #181c25;
|
||||
--app-dropdown-border-color: #202632;
|
||||
--app-dropdown-box-shadow: var(--app-box-shadow);
|
||||
--app-dropdown-color: var(--app-color);
|
||||
--app-dropdown-hover-background-color: #202632;
|
||||
--app-loading-spinner-opacity: 0.5;
|
||||
--app-modal-overlay-background-color: rgba(7.5, 8.5, 10, 0.75);
|
||||
--app-progress-background-color: #202632;
|
||||
--app-progress-color: var(--app-primary-background);
|
||||
--app-tooltip-background-color: var(--app-contrast-background);
|
||||
--app-tooltip-color: var(--app-contrast-inverse);
|
||||
--app-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
--app-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
:host(:not([data-theme]))
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="checkbox"],
|
||||
[type="radio"],
|
||||
[type="file"]
|
||||
),
|
||||
:root:not([data-theme])
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="checkbox"],
|
||||
[type="radio"],
|
||||
[type="file"]
|
||||
) {
|
||||
--app-form-element-focus-color: var(--app-primary-focus);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--app-background-color: rgb(19, 22.5, 30.5);
|
||||
--app-sidebar-background-color: rgb(18 20 27);
|
||||
--app-color: #c2c7d0;
|
||||
--app-text-selection-color: rgba(1, 170, 255, 0.1875);
|
||||
--app-muted-color: #7b8495;
|
||||
--app-muted-border-color: #202632;
|
||||
--app-primary-h-dark: var(--app-primary-h-light, var(--app-primary-h-base));
|
||||
--app-primary-s-dark: var(--app-primary-s-light, var(--app-primary-s-base));
|
||||
--app-primary-l-dark: var(--app-primary-l-light, var(--app-primary-l-base));
|
||||
--app-primary-h: var(--app-primary-h-dark, var(--app-primary-h-base));
|
||||
--app-primary-s: var(--app-primary-s-dark, var(--app-primary-s-base));
|
||||
--app-primary-l: var(--app-primary-l-dark, var(--app-primary-l-base));
|
||||
--app-primary-l-hover: -7%;
|
||||
--app-primary-l-hover-bg: -4%;
|
||||
--app-primary-l-focus: 16%;
|
||||
--app-primary: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s) var(--app-primary-l)
|
||||
);
|
||||
--app-primary-background: var(--app-primary);
|
||||
--app-primary-border: var(--app-primary-background);
|
||||
--app-primary-underline: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s) var(--app-primary-l) / 0.5
|
||||
);
|
||||
--app-primary-hover: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-hover))
|
||||
);
|
||||
--app-primary-hover-background: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-hover-bg))
|
||||
);
|
||||
--app-primary-hover-border: var(--app-primary-hover-background);
|
||||
--app-primary-hover-underline: var(--app-primary-hover);
|
||||
--app-primary-focus: hsl(
|
||||
var(--app-primary-h) var(--app-primary-s)
|
||||
calc(var(--app-primary-l) + var(--app-primary-l-focus)) / 0.5
|
||||
);
|
||||
--app-primary-inverse: #fff;
|
||||
--app-secondary-h-dark: var(
|
||||
--app-secondary-h-light,
|
||||
var(--app-secondary-h-base)
|
||||
);
|
||||
--app-secondary-s-dark: var(
|
||||
--app-secondary-s-light,
|
||||
var(--app-secondary-s-base)
|
||||
);
|
||||
--app-secondary-l-dark: var(
|
||||
--app-secondary-l-light,
|
||||
var(--app-secondary-l-base)
|
||||
);
|
||||
--app-secondary-h: var(--app-secondary-h-dark, var(--app-secondary-h-base));
|
||||
--app-secondary-s: var(--app-secondary-s-dark, var(--app-secondary-s-base));
|
||||
--app-secondary-l: var(--app-secondary-l-dark, var(--app-secondary-l-base));
|
||||
--app-secondary-l-hover: -6%;
|
||||
--app-secondary-l-hover-bg: -6%;
|
||||
--app-secondary-l-focus: 0%;
|
||||
--app-secondary: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l)
|
||||
);
|
||||
--app-secondary-background: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) - 4%)
|
||||
);
|
||||
--app-secondary-border: var(--app-secondary-background);
|
||||
--app-secondary-underline: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l) / 0.5
|
||||
);
|
||||
--app-secondary-hover: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) + var(--app-secondary-l-hover))
|
||||
);
|
||||
--app-secondary-hover-background: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s)
|
||||
calc(var(--app-secondary-l) + var(--app-secondary-l-hover-bg))
|
||||
);
|
||||
--app-secondary-hover-border: var(--app-secondary-hover-background);
|
||||
--app-secondary-hover-underline: var(--app-secondary-hover);
|
||||
--app-secondary-focus: hsl(
|
||||
var(--app-secondary-h) var(--app-secondary-s) var(--app-secondary-l) / 0.25
|
||||
);
|
||||
--app-secondary-inverse: #fff;
|
||||
--app-contrast: #dfe3eb;
|
||||
--app-contrast-background: #eff1f4;
|
||||
--app-contrast-border: var(--app-contrast-background);
|
||||
--app-contrast-underline: rgba(223, 227, 235, 0.5);
|
||||
--app-contrast-hover: #fff;
|
||||
--app-contrast-hover-background: #fff;
|
||||
--app-contrast-hover-border: var(--app-contrast-hover-background);
|
||||
--app-contrast-hover-underline: var(--app-contrast-hover);
|
||||
--app-contrast-focus: rgba(207, 213, 226, 0.25);
|
||||
--app-contrast-inverse: #000;
|
||||
--app-box-shadow:
|
||||
0.0145rem 0.029rem 0.174rem rgba(7, 8.5, 12, 0.01698),
|
||||
0.0335rem 0.067rem 0.402rem rgba(7, 8.5, 12, 0.024),
|
||||
0.0625rem 0.125rem 0.75rem rgba(7, 8.5, 12, 0.03),
|
||||
0.1125rem 0.225rem 1.35rem rgba(7, 8.5, 12, 0.036),
|
||||
0.2085rem 0.417rem 2.502rem rgba(7, 8.5, 12, 0.04302),
|
||||
0.5rem 1rem 6rem rgba(7, 8.5, 12, 0.06),
|
||||
0 0 0 0.0625rem rgba(7, 8.5, 12, 0.015);
|
||||
--app-h1-color: #f0f1f3;
|
||||
--app-h2-color: #e0e3e7;
|
||||
--app-h3-color: #c2c7d0;
|
||||
--app-h4-color: #b3b9c5;
|
||||
--app-h5-color: #a4acba;
|
||||
--app-h6-color: #8891a4;
|
||||
--app-mark-background-color: #014063;
|
||||
--app-mark-color: #fff;
|
||||
--app-ins-color: #62af9a;
|
||||
--app-del-color: rgb(205.5, 126, 123);
|
||||
--app-blockquote-border-color: var(--app-muted-border-color);
|
||||
--app-blockquote-footer-color: var(--app-muted-color);
|
||||
--app-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-table-border-color: var(--app-muted-border-color);
|
||||
--app-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375);
|
||||
--app-code-background-color: rgb(26, 30.5, 40.25);
|
||||
--app-code-color: #8891a4;
|
||||
--app-code-kbd-background-color: var(--app-color);
|
||||
--app-code-kbd-color: var(--app-background-color);
|
||||
--app-form-element-background-color: rgb(28, 33, 43.5);
|
||||
--app-form-element-selected-background-color: #2a3140;
|
||||
--app-form-element-border-color: #2a3140;
|
||||
--app-border: var(--app-form-element-border-color);
|
||||
--app-form-element-color: #e0e3e7;
|
||||
--app-form-element-placeholder-color: #8891a4;
|
||||
--app-form-element-active-background-color: rgb(26, 30.5, 40.25);
|
||||
--app-form-element-active-border-color: var(--app-primary-border);
|
||||
--app-form-element-focus-color: var(--app-primary-border);
|
||||
--app-form-element-disabled-opacity: 0.5;
|
||||
--app-form-element-invalid-border-color: rgb(149.5, 74, 80);
|
||||
--app-form-element-invalid-active-border-color: rgb(183.25, 63.5, 59);
|
||||
--app-form-element-invalid-focus-color: var(
|
||||
--app-form-element-invalid-active-border-color
|
||||
);
|
||||
--app-form-element-valid-border-color: #2a7b6f;
|
||||
--app-form-element-valid-active-border-color: rgb(22, 137, 105.5);
|
||||
--app-form-element-valid-focus-color: var(
|
||||
--app-form-element-valid-active-border-color
|
||||
);
|
||||
--app-notice-success-color: var(--app-form-element-valid-border-color);
|
||||
--app-notice-success-border-color: var(--app-form-element-valid-border-color);
|
||||
--app-notice-success-background-color: rgba(42, 123, 111, 0.2);
|
||||
--app-notice-warning-color: rgb(199, 140, 61);
|
||||
--app-notice-warning-border-color: rgb(199, 140, 61);
|
||||
--app-notice-warning-background-color: rgba(199, 140, 61, 0.2);
|
||||
--app-notice-info-color: var(--app-secondary);
|
||||
--app-notice-info-border-color: var(--app-secondary);
|
||||
--app-notice-info-background-color: rgba(114.4, 130.1, 165.1, 0.2);
|
||||
--app-notice-error-color: var(--app-form-element-invalid-border-color);
|
||||
--app-notice-error-border-color: var(--app-form-element-invalid-border-color);
|
||||
--app-notice-error-background-color: rgba(149.5, 74, 80, 0.2);
|
||||
--app-switch-background-color: #333c4e;
|
||||
--app-switch-checked-background-color: var(--app-primary-background);
|
||||
--app-switch-color: #fff;
|
||||
--app-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
--app-range-border-color: #202632;
|
||||
--app-range-active-border-color: #2a3140;
|
||||
--app-range-thumb-border-color: var(--app-background-color);
|
||||
--app-range-thumb-color: var(--app-secondary-background);
|
||||
--app-range-thumb-active-color: var(--app-primary-background);
|
||||
--app-accordion-border-color: var(--app-muted-border-color);
|
||||
--app-accordion-active-summary-color: var(--app-primary-hover);
|
||||
--app-accordion-close-summary-color: var(--app-color);
|
||||
--app-accordion-open-summary-color: var(--app-muted-color);
|
||||
--app-card-background-color: #181c25;
|
||||
--app-card-border-color: var(--app-card-background-color);
|
||||
--app-card-box-shadow: var(--app-box-shadow);
|
||||
--app-card-sectioning-background-color: rgb(26, 30.5, 40.25);
|
||||
--app-dropdown-background-color: #181c25;
|
||||
--app-dropdown-border-color: #202632;
|
||||
--app-dropdown-box-shadow: var(--app-box-shadow);
|
||||
--app-dropdown-color: var(--app-color);
|
||||
--app-dropdown-hover-background-color: #202632;
|
||||
--app-loading-spinner-opacity: 0.5;
|
||||
--app-modal-overlay-background-color: rgba(7.5, 8.5, 10, 0.75);
|
||||
--app-progress-background-color: #202632;
|
||||
--app-progress-color: var(--app-primary-background);
|
||||
--app-tooltip-background-color: var(--app-contrast-background);
|
||||
--app-tooltip-color: var(--app-contrast-inverse);
|
||||
--app-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
|
||||
--app-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(149.5, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[data-theme="dark"]
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="checkbox"],
|
||||
[type="radio"],
|
||||
[type="file"]
|
||||
) {
|
||||
--app-form-element-focus-color: var(--app-primary-focus);
|
||||
}
|
||||
|
||||
:host,
|
||||
:root {
|
||||
--app-scrollbar-width: 0px;
|
||||
}
|
||||
127
web/css/components/app-badges.css
Normal file
@@ -0,0 +1,127 @@
|
||||
:root {
|
||||
--badge-success-bg: #e6f6eb;
|
||||
--badge-success-color: #1f6a3a;
|
||||
--badge-success-border: #c9ead3;
|
||||
--badge-danger-bg: #fdecec;
|
||||
--badge-danger-color: #8f2c2c;
|
||||
--badge-danger-border: #f6caca;
|
||||
--badge-info-bg: #e9f0ff;
|
||||
--badge-info-color: #264db3;
|
||||
--badge-info-border: #cbd7ff;
|
||||
--badge-warn-bg: #fff5e6;
|
||||
--badge-warn-color: #9a5a00;
|
||||
--badge-warn-border: #ffd9a6;
|
||||
--badge-neutral-bg: #f2f4f7;
|
||||
--badge-neutral-color: #4a5568;
|
||||
--badge-neutral-border: #e1e6ee;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--badge-success-bg: #163a2a;
|
||||
--badge-success-color: #8fe0b0;
|
||||
--badge-success-border: #25523c;
|
||||
--badge-danger-bg: #3a1b1b;
|
||||
--badge-danger-color: #f2a3a3;
|
||||
--badge-danger-border: #5b2c2c;
|
||||
--badge-info-bg: #1a253f;
|
||||
--badge-info-color: #9fb6ff;
|
||||
--badge-info-border: #2a3a66;
|
||||
--badge-warn-bg: #3a2a14;
|
||||
--badge-warn-color: #ffd596;
|
||||
--badge-warn-border: #5b401d;
|
||||
--badge-neutral-bg: #222831;
|
||||
--badge-neutral-color: #cbd5e1;
|
||||
--badge-neutral-border: #2f3945;
|
||||
}
|
||||
|
||||
.badge[role="button"],
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35em;
|
||||
padding: 0.15em 0.6em;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge[data-copy="true"] {
|
||||
gap: 0.45em;
|
||||
padding-right: 0.4em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.badge .badge-copy-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
line-height: 1;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.badge .badge-copy-icon .bi {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.badge .badge-copy-icon .bi-check2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badge.is-copied .badge-copy-icon .bi-copy {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badge.is-copied .badge-copy-icon .bi-check2 {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.badge[data-copy="true"]:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.badge[data-variant="success"] {
|
||||
background: var(--badge-success-bg);
|
||||
color: var(--badge-success-color);
|
||||
border-color: var(--badge-success-border);
|
||||
}
|
||||
|
||||
.badge[data-variant="danger"] {
|
||||
background: var(--badge-danger-bg);
|
||||
color: var(--badge-danger-color);
|
||||
border-color: var(--badge-danger-border);
|
||||
}
|
||||
|
||||
.badge[data-variant="info"] {
|
||||
background: var(--badge-info-bg);
|
||||
color: var(--badge-info-color);
|
||||
border-color: var(--badge-info-border);
|
||||
}
|
||||
|
||||
.badge[data-variant="warn"] {
|
||||
background: var(--badge-warn-bg);
|
||||
color: var(--badge-warn-color);
|
||||
border-color: var(--badge-warn-border);
|
||||
}
|
||||
.badge[data-variant="warning"] {
|
||||
background: var(--badge-warn-bg);
|
||||
color: var(--badge-warn-color);
|
||||
border-color: var(--badge-warn-border);
|
||||
}
|
||||
|
||||
.badge[data-variant="neutral"] {
|
||||
background: var(--badge-neutral-bg);
|
||||
color: var(--badge-neutral-color);
|
||||
border-color: var(--badge-neutral-border);
|
||||
}
|
||||
|
||||
.badge-list {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
66
web/css/components/app-blockquote.css
Normal file
@@ -0,0 +1,66 @@
|
||||
:root {
|
||||
--app-blockquote-background-color: var(
|
||||
--app-form-element-active-background-color
|
||||
);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
--blockquote-bg: var(--app-blockquote-background-color);
|
||||
--blockquote-color: inherit;
|
||||
--blockquote-border: var(--app-blockquote-border-color);
|
||||
--blockquote-footer-color: var(--app-blockquote-footer-color);
|
||||
|
||||
display: block;
|
||||
margin: var(--app-typography-spacing-vertical) 0;
|
||||
padding: var(--app-spacing);
|
||||
border-right: none;
|
||||
border-left: 0.25rem solid var(--app-blockquote-border-color);
|
||||
border-inline-start: 0.25rem solid var(--app-blockquote-border-color);
|
||||
border-inline-end: none;
|
||||
background-color: var(--blockquote-bg);
|
||||
color: var(--blockquote-color);
|
||||
border-left-color: var(--blockquote-border);
|
||||
border-inline-start-color: var(--blockquote-border);
|
||||
border-radius: var(--app-border-radius);
|
||||
}
|
||||
|
||||
blockquote footer {
|
||||
margin-top: calc(var(--app-typography-spacing-vertical) * 0.5);
|
||||
color: var(--blockquote-footer-color);
|
||||
}
|
||||
|
||||
blockquote[data-variant="success"] {
|
||||
--blockquote-bg: var(--badge-success-bg);
|
||||
--blockquote-color: var(--badge-success-color);
|
||||
--blockquote-border: var(--badge-success-border);
|
||||
--blockquote-footer-color: var(--badge-success-color);
|
||||
}
|
||||
|
||||
blockquote[data-variant="danger"] {
|
||||
--blockquote-bg: var(--badge-danger-bg);
|
||||
--blockquote-color: var(--badge-danger-color);
|
||||
--blockquote-border: var(--badge-danger-border);
|
||||
--blockquote-footer-color: var(--badge-danger-color);
|
||||
}
|
||||
|
||||
blockquote[data-variant="info"] {
|
||||
--blockquote-bg: var(--badge-info-bg);
|
||||
--blockquote-color: var(--badge-info-color);
|
||||
--blockquote-border: var(--badge-info-border);
|
||||
--blockquote-footer-color: var(--badge-info-color);
|
||||
}
|
||||
|
||||
blockquote[data-variant="warn"],
|
||||
blockquote[data-variant="warning"] {
|
||||
--blockquote-bg: var(--badge-warn-bg);
|
||||
--blockquote-color: var(--badge-warn-color);
|
||||
--blockquote-border: var(--badge-warn-border);
|
||||
--blockquote-footer-color: var(--badge-warn-color);
|
||||
}
|
||||
|
||||
blockquote[data-variant="neutral"] {
|
||||
--blockquote-bg: var(--badge-neutral-bg);
|
||||
--blockquote-color: var(--badge-neutral-color);
|
||||
--blockquote-border: var(--badge-neutral-border);
|
||||
--blockquote-footer-color: var(--badge-neutral-color);
|
||||
}
|
||||
31
web/css/components/app-brand.css
Normal file
@@ -0,0 +1,31 @@
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
img.brand-logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
color: var(--app-contrast);
|
||||
}
|
||||
|
||||
.brand-info {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.brand-slogan {
|
||||
font-size: 0.85em;
|
||||
color: var(--app-muted-color);
|
||||
}
|
||||
|
||||
.badge:empty {
|
||||
padding: 0;
|
||||
}
|
||||
12
web/css/components/app-breadcrumb.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.app-breadcrumb {
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.app-breadcrumb a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
44
web/css/components/app-buttons.css
Normal file
@@ -0,0 +1,44 @@
|
||||
.danger:is(button, [type="submit"], [type="button"], [role="button"]) {
|
||||
--app-background-color: var(--app-notice-error-border-color);
|
||||
--app-border-color: var(--app-notice-error-border-color);
|
||||
--app-color: var(--app-secondary-inverse);
|
||||
}
|
||||
|
||||
.danger:is(button, [type="submit"], [type="button"], [role="button"]):is(
|
||||
[aria-current]:not([aria-current="false"]),
|
||||
:hover,
|
||||
:active,
|
||||
:focus
|
||||
) {
|
||||
--app-background-color: var(--app-form-element-invalid-active-border-color);
|
||||
--app-border-color: var(--app-form-element-invalid-active-border-color);
|
||||
--app-color: var(--app-secondary-inverse);
|
||||
}
|
||||
|
||||
.danger:is(button, [type="submit"], [type="button"], [role="button"]).outline {
|
||||
--app-background-color: transparent;
|
||||
--app-border-color: var(--app-notice-error-border-color);
|
||||
--app-color: var(--app-notice-error-border-color);
|
||||
}
|
||||
|
||||
.danger:is(
|
||||
button,
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
[role="button"]
|
||||
).outline:is(
|
||||
[aria-current]:not([aria-current="false"]),
|
||||
:hover,
|
||||
:active,
|
||||
:focus
|
||||
) {
|
||||
--app-background-color: var(--app-notice-error-background-color);
|
||||
--app-border-color: var(--app-notice-error-border-color);
|
||||
--app-color: var(--app-notice-error-border-color);
|
||||
}
|
||||
|
||||
.danger:is(button, [type="submit"], [type="button"], [role="button"]):focus {
|
||||
--app-box-shadow:
|
||||
var(--app-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),
|
||||
0 0 0 var(--app-outline-width) var(--app-form-element-invalid-focus-color);
|
||||
}
|
||||
55
web/css/components/app-dashboard-titlebar.css
Normal file
@@ -0,0 +1,55 @@
|
||||
.app-dashboard-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-block-end: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-dashboard-titlebar h1,
|
||||
.app-dashboard-titlebar h2,
|
||||
.app-dashboard-titlebar h3,
|
||||
.app-dashboard-titlebar h4,
|
||||
.app-dashboard-titlebar h5,
|
||||
.app-dashboard-titlebar h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-dashboard-titlebar button,
|
||||
.app-dashboard-titlebar [role="button"] {
|
||||
--app-button-padding-horizontal: 10px;
|
||||
--app-button-padding-vertical: 6px;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-dashboard-titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-dashboard-titlebar h1 i {
|
||||
background: var(--app-muted-border-color);
|
||||
width: 35px;
|
||||
height: 29px;
|
||||
font-size: 15px;
|
||||
border-radius: var(--app-border-radius);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--app-contrast);
|
||||
}
|
||||
|
||||
.app-dashboard-titlebar h1:has(i) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.app-dashboard-titlebar h1 a:hover i {
|
||||
background: var(--app-border);
|
||||
}
|
||||
87
web/css/components/app-details-titlebar.css
Normal file
@@ -0,0 +1,87 @@
|
||||
.app-details-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
padding-block-end: var(--app-spacing);
|
||||
margin-bottom: calc(var(--app-spacing) * 2);
|
||||
}
|
||||
|
||||
.app-details-titlebar h1,
|
||||
.app-details-titlebar h2,
|
||||
.app-details-titlebar h3,
|
||||
.app-details-titlebar h4,
|
||||
.app-details-titlebar h5,
|
||||
.app-details-titlebar h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-details-titlebar button,
|
||||
.app-details-titlebar [role="button"] {
|
||||
--app-button-padding-horizontal: 10px;
|
||||
--app-button-padding-vertical: 6px;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-details-titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-details-titlebar h1 i {
|
||||
background: var(--app-background-color);
|
||||
width: 35px;
|
||||
height: 29px;
|
||||
font-size: 15px;
|
||||
border-radius: var(--app-border-radius);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--app-contrast);
|
||||
border: 1px solid var(--app-border);
|
||||
color: var(--app-muted-color);
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 2px;
|
||||
}
|
||||
|
||||
.app-details-titlebar h1:has(i) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
line-height: 1;
|
||||
}
|
||||
.app-details-titlebar details.dropdown {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown summary:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown li {
|
||||
padding: 0!important;
|
||||
margin: 0!important;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown ul {
|
||||
padding: 0!important;
|
||||
margin: 0!important;
|
||||
}
|
||||
|
||||
.app-details-titlebar details.dropdown li button,
|
||||
.app-details-titlebar details.dropdown li [role=button] {
|
||||
margin: 0!important;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
137
web/css/components/app-details.css
Normal file
@@ -0,0 +1,137 @@
|
||||
.app-details-container aside {
|
||||
padding-top: var(--app-spacing);
|
||||
}
|
||||
|
||||
.user-avatar-block,
|
||||
.entity-avatar-block {
|
||||
--avatar-size: 96px;
|
||||
--avatar-ratio: 1 / 1;
|
||||
display: grid;
|
||||
gap: calc(var(--app-spacing) * 0.75);
|
||||
margin-bottom: calc(var(--app-spacing) * 1.25);
|
||||
width: var(--avatar-size);
|
||||
aspect-ratio: var(--avatar-ratio);
|
||||
}
|
||||
|
||||
.user-avatar-image,
|
||||
.user-avatar-placeholder,
|
||||
.entity-avatar-image,
|
||||
.entity-avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--app-muted-border-color);
|
||||
background: var(--app-card-background-color);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
color: var(--app-muted-color);
|
||||
}
|
||||
|
||||
.user-avatar-placeholder,
|
||||
.entity-avatar-placeholder {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.user-avatar-image {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.entity-avatar-image {
|
||||
object-fit: contain;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.avatar-round .user-avatar-image,
|
||||
.avatar-round .user-avatar-placeholder,
|
||||
.avatar-round .entity-avatar-image,
|
||||
.avatar-round .entity-avatar-placeholder {
|
||||
border-radius: 999px;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.avatar-square .user-avatar-image,
|
||||
.avatar-square .user-avatar-placeholder,
|
||||
.avatar-square .entity-avatar-image,
|
||||
.avatar-square .entity-avatar-placeholder {
|
||||
border-radius: var(--app-radius);
|
||||
}
|
||||
|
||||
.avatar-ratio-1-1 {
|
||||
--avatar-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.avatar-ratio-3-2 {
|
||||
--avatar-ratio: 3 / 2;
|
||||
}
|
||||
|
||||
.avatar-ratio-4-3 {
|
||||
--avatar-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.avatar-ratio-16-9 {
|
||||
--avatar-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.avatar-borderless .user-avatar-image,
|
||||
.avatar-borderless .user-avatar-placeholder,
|
||||
.avatar-borderless .entity-avatar-image,
|
||||
.avatar-borderless .entity-avatar-placeholder {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.avatar-size-auto {
|
||||
width: auto;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
.avatar-size-auto .user-avatar-image,
|
||||
.avatar-size-auto .user-avatar-placeholder,
|
||||
.avatar-size-auto .entity-avatar-image,
|
||||
.avatar-size-auto .entity-avatar-placeholder {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.app-details-container > section > * {
|
||||
padding-inline: calc(var(--app-spacing) * 2);
|
||||
}
|
||||
|
||||
.app-details-container .app-breadcrumb {
|
||||
padding-top: calc(var(--app-spacing) * 2);
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
background: var(--app-background-color);
|
||||
}
|
||||
|
||||
.app-details-container .app-details-titlebar {
|
||||
position: sticky;
|
||||
top: 69px;
|
||||
background: var(--app-background-color);
|
||||
}
|
||||
|
||||
@media (min-width: 968px) {
|
||||
/* .app-details-container > section {
|
||||
padding: calc(var(--app-spacing) * 2) calc(var(--app-spacing) * 2)
|
||||
calc(var(--app-spacing) * 2) calc(var(--app-spacing) * 0);
|
||||
} */
|
||||
.app-details-container:has(aside) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(0, 300px);
|
||||
min-height: 82vh;
|
||||
}
|
||||
.app-details-container.is-aside-collapsed {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.app-details-container.is-aside-collapsed aside {
|
||||
display: none;
|
||||
}
|
||||
.app-details-container aside {
|
||||
border-left: 1px solid var(--app-border);
|
||||
padding: calc(var(--app-spacing) * 2);
|
||||
border-top: none;
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
119
web/css/components/app-flash.css
Normal file
@@ -0,0 +1,119 @@
|
||||
.notice {
|
||||
--app-notice-border-color: var(--app-muted-border-color);
|
||||
--app-notice-background-color: var(--app-card-background-color);
|
||||
--app-notice-accent-color: var(--app-color);
|
||||
border: var(--app-border-width) solid var(--app-notice-border-color);
|
||||
border-radius: var(--app-border-radius);
|
||||
background: var(--app-notice-background-color);
|
||||
padding: calc(var(--app-spacing) * .5) calc(var(--app-spacing) * 1);
|
||||
padding-right: calc(var(--app-spacing) * .5);
|
||||
margin-bottom: var(--app-spacing);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--app-spacing);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: var(--app-notice-border-color);
|
||||
}
|
||||
|
||||
#async-messages .notice {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
padding-right: 5px;
|
||||
padding-left: 15px;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: var(--app-notice-border-color);
|
||||
}
|
||||
|
||||
|
||||
.notice strong {
|
||||
color: var(--app-notice-accent-color);
|
||||
}
|
||||
|
||||
.notice[data-variant=success] {
|
||||
--app-notice-border-color: var(--app-notice-success-border-color);
|
||||
--app-notice-background-color: var(--app-notice-success-background-color);
|
||||
--app-notice-accent-color: var(--app-notice-success-color);
|
||||
}
|
||||
|
||||
.notice[data-variant=warning] {
|
||||
--app-notice-border-color: var(--app-notice-warning-border-color);
|
||||
--app-notice-background-color: var(--app-notice-warning-background-color);
|
||||
--app-notice-accent-color: var(--app-notice-warning-color);
|
||||
}
|
||||
|
||||
.notice[data-variant=info] {
|
||||
--app-notice-border-color: var(--app-notice-info-border-color);
|
||||
--app-notice-background-color: var(--app-notice-info-background-color);
|
||||
--app-notice-accent-color: var(--app-notice-info-color);
|
||||
}
|
||||
|
||||
.notice[data-variant=error] {
|
||||
--app-notice-border-color: var(--app-notice-error-border-color);
|
||||
--app-notice-background-color: var(--app-notice-error-background-color);
|
||||
--app-notice-accent-color: var(--app-notice-error-color);
|
||||
}
|
||||
|
||||
.notice.flash-timed::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
background: var(--app-notice-border-color);
|
||||
transform-origin: left;
|
||||
transform: scaleX(0);
|
||||
animation: flash-progress var(--flash-timeout, 4000ms) linear forwards;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.notice.flash-timed:hover::after {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@keyframes flash-progress {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.notice.flash-timed::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.notice button[type=submit] {
|
||||
border: 1px solid var(--app-notice-border-color) !important;
|
||||
background: var(--app-notice-background-color);
|
||||
color: var(--app-notice-border-color);
|
||||
}
|
||||
|
||||
.notice ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.notice li:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-details-errors {
|
||||
padding-block-start: 1rem;
|
||||
}
|
||||
|
||||
.flash-stack:has(.notice) {
|
||||
margin-top: var(--app-spacing);
|
||||
}
|
||||
62
web/css/components/app-forms.css
Normal file
@@ -0,0 +1,62 @@
|
||||
.form-hint {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
ul.form-hint-list {
|
||||
margin: 0;
|
||||
padding-left: var(--app-spacing);
|
||||
}
|
||||
|
||||
ul.form-hint-list li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul.form-hint-list li.is-valid {
|
||||
color: var(--app-form-element-valid-border-color);
|
||||
}
|
||||
|
||||
ul.form-hint-list li.is-invalid {
|
||||
color: var(--app-form-element-invalid-border-color);
|
||||
}
|
||||
|
||||
ul.form-hint-list li.is-valid:after {
|
||||
content: "\F26B";
|
||||
font-family: "Bootstrap-icons";
|
||||
margin-left: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
ul.form-hint-list li.is-invalid:after {
|
||||
content: "\F33A";
|
||||
font-family: "Bootstrap-icons";
|
||||
margin-left: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
label:has([type="checkbox"], [type="radio"]) {
|
||||
display: flex;
|
||||
line-height: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
padding: 0;
|
||||
width: 42px;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
height: 42px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
label:has(input[type="color"]) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
small.muted {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: var(--app-muted-color);
|
||||
}
|
||||
|
||||
7
web/css/components/app-list-table.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.app-list-table table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-list-table {
|
||||
white-space: nowrap;
|
||||
}
|
||||
37
web/css/components/app-list-tabs.css
Normal file
@@ -0,0 +1,37 @@
|
||||
:root {
|
||||
--list-tabs-border: var(--app-border);
|
||||
--list-tabs-active: var(--app-primary);
|
||||
--list-tabs-muted: var(--app-muted-color);
|
||||
}
|
||||
|
||||
.app-list-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--app-spacing);
|
||||
border-bottom: 1px solid var(--list-tabs-border);
|
||||
}
|
||||
|
||||
.app-list-tabs button,
|
||||
.app-list-tabs a {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 5px 10px 5px 10px;
|
||||
color: var(--list-tabs-muted);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.app-list-tabs button:hover,
|
||||
.app-list-tabs button:focus-visible,
|
||||
.app-list-tabs a:hover,
|
||||
.app-list-tabs a:focus-visible {
|
||||
color: var(--app-color);
|
||||
}
|
||||
|
||||
.app-list-tabs button.is-active,
|
||||
.app-list-tabs a.is-active {
|
||||
color: var(--app-color);
|
||||
border-color: var(--list-tabs-active);
|
||||
}
|
||||
79
web/css/components/app-list-titlebar.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.app-list-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: calc(var(--app-spacing) * 1);
|
||||
}
|
||||
|
||||
.app-list-titlebar h1,
|
||||
.app-list-titlebar h2,
|
||||
.app-list-titlebar h3,
|
||||
.app-list-titlebar h4,
|
||||
.app-list-titlebar h5,
|
||||
.app-list-titlebar h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-list-titlebar button,
|
||||
.app-list-titlebar [role="button"] {
|
||||
--app-button-padding-horizontal: 10px;
|
||||
--app-button-padding-vertical: 6px;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-list-titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.app-list-titlebar h1 i {
|
||||
background: var(--app-muted-border-color);
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
font-size: 16px;
|
||||
border-radius: var(--app-border-radius);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-list-titlebar h1:has(i) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown summary:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown li {
|
||||
padding: 0!important;
|
||||
margin: 0!important;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown ul {
|
||||
padding: 0!important;
|
||||
margin: 0!important;
|
||||
}
|
||||
|
||||
.app-list-titlebar details.dropdown li button, .app-list-titlebar details.dropdown li [role=button] {
|
||||
margin: 0 !important;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
69
web/css/components/app-list-toolbar.css
Normal file
@@ -0,0 +1,69 @@
|
||||
.app-list-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: calc(var(--app-spacing) * 1);
|
||||
}
|
||||
|
||||
.app-list-toolbar > * {
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
--app-form-element-spacing-horizontal: 10px;
|
||||
--app-form-element-spacing-vertical: 6px;
|
||||
font-size: 13px !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.app-list-toolbar > * > * {
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
--app-form-element-spacing-horizontal: 10px;
|
||||
--app-form-element-spacing-vertical: 6px;
|
||||
font-size: 13px !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.app-list-toolbar
|
||||
input:not(
|
||||
[type="checkbox"],
|
||||
.app-list-toolbar [type="radio"],
|
||||
.app-list-toolbar [type="range"],
|
||||
.app-list-toolbar [type="file"]
|
||||
):is(
|
||||
[type="date"],
|
||||
[type="datetime-local"],
|
||||
[type="month"],
|
||||
[type="time"],
|
||||
[type="week"]
|
||||
) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.app-list-toolbar > label > span {
|
||||
display: block;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.app-list-toolbar
|
||||
.multi-select
|
||||
.multi-select-header
|
||||
.multi-select-header-placeholder {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.app-list-toolbar .multi-select .multi-select-header, .app-list-toolbar .multi-select .multi-select-option, .app-list-toolbar .multi-select .multi-select-all {
|
||||
min-height: unset;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.app-list-toolbar .multi-select {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
.app-list-toolbar span.multi-select-header-option {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
padding: 0 !important;
|
||||
padding-left: 3px !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
10
web/css/components/app-page-copy.css
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
|
||||
.app-page-copy .app-field {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
|
||||
.app-page-copy .app-field [role="group"] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
74
web/css/components/app-page-editor.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.app-page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--app-spacing);
|
||||
margin-bottom: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-page-header > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-page-header > div > * {
|
||||
margin-bottom: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.app-page-header button,
|
||||
.app-page-header input {
|
||||
margin-bottom: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.app-page-header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
label.app-field > span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label.app-field > small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.page-editor-actions {
|
||||
margin-top: var(--app-spacing);
|
||||
display: flex;
|
||||
gap: var(--app-spacing);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.page-editor[data-edit-mode="view"] .page-editor-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-editor[data-edit-mode="view"] .ce-toolbar,
|
||||
.page-editor[data-edit-mode="view"] .ce-inline-toolbar,
|
||||
.page-editor[data-edit-mode="view"] .ce-popover {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ce-block__content, .ce-toolbar__content {
|
||||
max-width: 90%!important;
|
||||
}
|
||||
|
||||
.ce-editorjsColumns_wrapper .ce-toolbar__content,
|
||||
.ce-editorjsColumns_wrapper .ce-block__content {
|
||||
max-width: 100% ! Important;
|
||||
}
|
||||
|
||||
.ce-popover-item[data-item-name="columns"] svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.app-details-aside-section button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
570
web/css/components/app-profile.css
Normal file
@@ -0,0 +1,570 @@
|
||||
.app-profile-card {
|
||||
border: 1px solid var(--app-border);
|
||||
background: var(--app-card-background-color);
|
||||
overflow: hidden;
|
||||
border-radius: var(--app-border-radius);
|
||||
margin-bottom: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-profile-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 0 calc(var(--app-spacing) * 1) 0 calc(var(--app-spacing) * 1);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-top: -42px;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app-profile-meta h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-profile-meta p {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--app-muted-color);
|
||||
}
|
||||
|
||||
.app-profile-body {
|
||||
border: 1px solid var(--app-border);
|
||||
background: var(--app-card-background-color);
|
||||
overflow: hidden;
|
||||
border-radius: var(--app-border-radius);
|
||||
margin-bottom: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-profile-body [data-tab-panel] {
|
||||
padding-inline: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-profile-body [data-tab-panel]:not(:has(p)) {
|
||||
padding-block-end: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-profile-table {
|
||||
width: 100%;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-border-radius);
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-profile-table th,
|
||||
.app-profile-table td {
|
||||
padding: 0.55rem 0.75rem;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.app-profile-table thead th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-profile-table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
@keyframes ani-app-profile-banner {
|
||||
0% {
|
||||
--c-0: hsla(212, 0%, 0%, 1);
|
||||
--s-start-0: 14.489998991212337%;
|
||||
--s-end-0: 72%;
|
||||
--y-0: 93%;
|
||||
--x-0: 93%;
|
||||
--c-1: hsla(212, 0%, 0%, 1);
|
||||
--s-start-1: 0%;
|
||||
--s-end-1: 45%;
|
||||
--y-1: 9%;
|
||||
--x-1: 26%;
|
||||
--x-2: 15%;
|
||||
--s-start-2: 2.9253667596993065%;
|
||||
--s-end-2: 22.388851682060018%;
|
||||
--c-2: hsla(257, 91%, 27%, 0.35);
|
||||
--y-2: 79%;
|
||||
--x-3: 40%;
|
||||
--s-start-3: 3.985353824694249%;
|
||||
--s-end-3: 47.580278608924694%;
|
||||
--y-3: 104%;
|
||||
--c-3: hsla(212, 100%, 50%, 0.5);
|
||||
--y-4: 60%;
|
||||
--x-4: 0%;
|
||||
--c-4: hsla(224, 72%, 36%, 1);
|
||||
--s-start-4: 2.391200382592061%;
|
||||
--s-end-4: 29.307684556768592%;
|
||||
--s-start-5: 2.9253667596993065%;
|
||||
--s-end-5: 22.388851682060018%;
|
||||
--y-5: 37%;
|
||||
--c-5: hsla(248, 52%, 24%, 1);
|
||||
--x-5: 92%;
|
||||
--x-6: 101%;
|
||||
--y-6: 16%;
|
||||
--s-start-6: 13.173642363290591%;
|
||||
--s-end-6: 31.747336520355095%;
|
||||
--c-6: hsla(212, 100%, 50%, 0.19);
|
||||
--y-7: 13%;
|
||||
--s-start-7: 1%;
|
||||
--s-end-7: 31%;
|
||||
--x-7: 90%;
|
||||
--c-7: hsla(227, 98%, 53%, 1);
|
||||
--y-8: 56%;
|
||||
--s-start-8: 3.985353824694249%;
|
||||
--s-end-8: 13.103042116379756%;
|
||||
--c-8: hsla(166, 71%, 60%, 0.32);
|
||||
--x-8: 104%;
|
||||
--c-9: hsla(219, 83%, 23%, 0.18);
|
||||
--s-start-9: 18.597054544690312%;
|
||||
--s-end-9: 31%;
|
||||
--x-9: 97%;
|
||||
--y-9: 19%;
|
||||
}
|
||||
|
||||
100% {
|
||||
--c-0: hsla(306, 0%, 0%, 1);
|
||||
--s-start-0: 2.391200382592061%;
|
||||
--s-end-0: 43.902064173373226%;
|
||||
--y-0: 9%;
|
||||
--x-0: 7%;
|
||||
--c-1: hsla(306, 0%, 0%, 1);
|
||||
--s-start-1: 9%;
|
||||
--s-end-1: 54.805582404585024%;
|
||||
--y-1: 93%;
|
||||
--x-1: 96%;
|
||||
--x-2: -2%;
|
||||
--s-start-2: 3%;
|
||||
--s-end-2: 26.722813338714598%;
|
||||
--c-2: hsla(166, 72%, 60%, 1);
|
||||
--y-2: 103%;
|
||||
--x-3: 33%;
|
||||
--s-start-3: 2.391200382592061%;
|
||||
--s-end-3: 32.0689540200964%;
|
||||
--y-3: 82%;
|
||||
--c-3: hsla(180, 100%, 50%, 0.26);
|
||||
--y-4: 81%;
|
||||
--x-4: 37%;
|
||||
--c-4: hsla(212, 88%, 26%, 0.58);
|
||||
--s-start-4: 4.40642490323111%;
|
||||
--s-end-4: 37.23528104246256%;
|
||||
--s-start-5: 3%;
|
||||
--s-end-5: 32.537089799783296%;
|
||||
--y-5: 99%;
|
||||
--c-5: hsla(271, 98%, 53%, 0.31);
|
||||
--x-5: 54%;
|
||||
--x-6: 104%;
|
||||
--y-6: 43%;
|
||||
--s-start-6: 6%;
|
||||
--s-end-6: 42.501105312974815%;
|
||||
--c-6: hsla(262, 100%, 50%, 0.15);
|
||||
--y-7: -16%;
|
||||
--s-start-7: 5%;
|
||||
--s-end-7: 13.10107024898374%;
|
||||
--x-7: 104%;
|
||||
--c-7: hsla(298, 36%, 23%, 1);
|
||||
--y-8: 30%;
|
||||
--s-start-8: 2.391200382592061%;
|
||||
--s-end-8: 27.141813016850573%;
|
||||
--c-8: hsla(180, 100%, 50%, 0.11);
|
||||
--x-8: 97%;
|
||||
--c-9: hsla(219, 83%, 23%, 0.59);
|
||||
--s-start-9: 5%;
|
||||
--s-end-9: 21.32164536610654%;
|
||||
--x-9: 78%;
|
||||
--y-9: 4%;
|
||||
}
|
||||
}
|
||||
|
||||
@property --c-0 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(212, 0%, 0%, 1);
|
||||
}
|
||||
|
||||
@property --s-start-0 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 14.489998991212337%;
|
||||
}
|
||||
|
||||
@property --s-end-0 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 72%;
|
||||
}
|
||||
|
||||
@property --y-0 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 93%;
|
||||
}
|
||||
|
||||
@property --x-0 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 93%;
|
||||
}
|
||||
|
||||
@property --c-1 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(212, 0%, 0%, 1);
|
||||
}
|
||||
|
||||
@property --s-start-1 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 0%;
|
||||
}
|
||||
|
||||
@property --s-end-1 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 45%;
|
||||
}
|
||||
|
||||
@property --y-1 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 9%;
|
||||
}
|
||||
|
||||
@property --x-1 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 26%;
|
||||
}
|
||||
|
||||
@property --x-2 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 15%;
|
||||
}
|
||||
|
||||
@property --s-start-2 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 2.9253667596993065%;
|
||||
}
|
||||
|
||||
@property --s-end-2 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 22.388851682060018%;
|
||||
}
|
||||
|
||||
@property --c-2 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(257, 91%, 27%, 0.35);
|
||||
}
|
||||
|
||||
@property --y-2 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 79%;
|
||||
}
|
||||
|
||||
@property --x-3 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 40%;
|
||||
}
|
||||
|
||||
@property --s-start-3 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 3.985353824694249%;
|
||||
}
|
||||
|
||||
@property --s-end-3 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 47.580278608924694%;
|
||||
}
|
||||
|
||||
@property --y-3 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 104%;
|
||||
}
|
||||
|
||||
@property --c-3 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(212, 100%, 50%, 0.5);
|
||||
}
|
||||
|
||||
@property --y-4 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 60%;
|
||||
}
|
||||
|
||||
@property --x-4 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 0%;
|
||||
}
|
||||
|
||||
@property --c-4 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(224, 72%, 36%, 1);
|
||||
}
|
||||
|
||||
@property --s-start-4 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 2.391200382592061%;
|
||||
}
|
||||
|
||||
@property --s-end-4 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 29.307684556768592%;
|
||||
}
|
||||
|
||||
@property --s-start-5 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 2.9253667596993065%;
|
||||
}
|
||||
|
||||
@property --s-end-5 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 22.388851682060018%;
|
||||
}
|
||||
|
||||
@property --y-5 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 37%;
|
||||
}
|
||||
|
||||
@property --c-5 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(248, 52%, 24%, 1);
|
||||
}
|
||||
|
||||
@property --x-5 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 92%;
|
||||
}
|
||||
|
||||
@property --x-6 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 101%;
|
||||
}
|
||||
|
||||
@property --y-6 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 16%;
|
||||
}
|
||||
|
||||
@property --s-start-6 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 13.173642363290591%;
|
||||
}
|
||||
|
||||
@property --s-end-6 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 31.747336520355095%;
|
||||
}
|
||||
|
||||
@property --c-6 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(212, 100%, 50%, 0.19);
|
||||
}
|
||||
|
||||
@property --y-7 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 13%;
|
||||
}
|
||||
|
||||
@property --s-start-7 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 1%;
|
||||
}
|
||||
|
||||
@property --s-end-7 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 31%;
|
||||
}
|
||||
|
||||
@property --x-7 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 90%;
|
||||
}
|
||||
|
||||
@property --c-7 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(227, 98%, 53%, 1);
|
||||
}
|
||||
|
||||
@property --y-8 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 56%;
|
||||
}
|
||||
|
||||
@property --s-start-8 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 3.985353824694249%;
|
||||
}
|
||||
|
||||
@property --s-end-8 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 13.103042116379756%;
|
||||
}
|
||||
|
||||
@property --c-8 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(166, 71%, 60%, 0.32);
|
||||
}
|
||||
|
||||
@property --x-8 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 104%;
|
||||
}
|
||||
|
||||
@property --c-9 {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: hsla(219, 83%, 23%, 0.18);
|
||||
}
|
||||
|
||||
@property --s-start-9 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 18.597054544690312%;
|
||||
}
|
||||
|
||||
@property --s-end-9 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 31%;
|
||||
}
|
||||
|
||||
@property --x-9 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 97%;
|
||||
}
|
||||
|
||||
@property --y-9 {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 19%;
|
||||
}
|
||||
|
||||
.app-profile-banner {
|
||||
--c-0: hsla(212, 0%, 0%, 1);
|
||||
--y-0: 93%;
|
||||
--x-0: 93%;
|
||||
--c-1: hsla(212, 0%, 0%, 1);
|
||||
--y-1: 9%;
|
||||
--x-1: 26%;
|
||||
--x-2: 15%;
|
||||
--c-2: hsla(257, 91%, 27%, 0.35);
|
||||
--y-2: 79%;
|
||||
--x-3: 40%;
|
||||
--y-3: 104%;
|
||||
--c-3: hsla(212, 100%, 50%, 0.5);
|
||||
--y-4: 60%;
|
||||
--x-4: 0%;
|
||||
--c-4: hsla(224, 72%, 36%, 1);
|
||||
--y-5: 37%;
|
||||
--c-5: hsla(248, 52%, 24%, 1);
|
||||
--x-5: 92%;
|
||||
--x-6: 101%;
|
||||
--y-6: 16%;
|
||||
--c-6: hsla(212, 100%, 50%, 0.19);
|
||||
--y-7: 13%;
|
||||
--x-7: 90%;
|
||||
--c-7: hsla(227, 98%, 53%, 1);
|
||||
--y-8: 56%;
|
||||
--c-8: hsla(166, 71%, 60%, 0.32);
|
||||
--x-8: 104%;
|
||||
--c-9: hsla(219, 83%, 23%, 0.18);
|
||||
--x-9: 97%;
|
||||
--y-9: 19%;
|
||||
background-color: hsla(305, 0%, 0%, 1);
|
||||
background-image:
|
||||
radial-gradient(
|
||||
circle at var(--x-0) var(--y-0),
|
||||
var(--c-0) var(--s-start-0),
|
||||
transparent var(--s-end-0)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-1) var(--y-1),
|
||||
var(--c-1) var(--s-start-1),
|
||||
transparent var(--s-end-1)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-2) var(--y-2),
|
||||
var(--c-2) var(--s-start-2),
|
||||
transparent var(--s-end-2)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-3) var(--y-3),
|
||||
var(--c-3) var(--s-start-3),
|
||||
transparent var(--s-end-3)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-4) var(--y-4),
|
||||
var(--c-4) var(--s-start-4),
|
||||
transparent var(--s-end-4)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-5) var(--y-5),
|
||||
var(--c-5) var(--s-start-5),
|
||||
transparent var(--s-end-5)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-6) var(--y-6),
|
||||
var(--c-6) var(--s-start-6),
|
||||
transparent var(--s-end-6)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-7) var(--y-7),
|
||||
var(--c-7) var(--s-start-7),
|
||||
transparent var(--s-end-7)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-8) var(--y-8),
|
||||
var(--c-8) var(--s-start-8),
|
||||
transparent var(--s-end-8)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at var(--x-9) var(--y-9),
|
||||
var(--c-9) var(--s-start-9),
|
||||
transparent var(--s-end-9)
|
||||
);
|
||||
animation: ani-app-profile-banner 10s linear infinite alternate;
|
||||
background-blend-mode:
|
||||
normal, normal, normal, normal, normal, normal, normal, normal, normal,
|
||||
normal;
|
||||
will-change: transform, opacity;
|
||||
contain: paint;
|
||||
height: 160px;
|
||||
}
|
||||
137
web/css/components/app-search.css
Normal file
@@ -0,0 +1,137 @@
|
||||
.app-search {
|
||||
padding: 0 var(--app-spacing);
|
||||
}
|
||||
|
||||
ul.app-search-results {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul.app-search-results > li > a {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ul.app-search-results .badge {
|
||||
font-size: 10px;
|
||||
padding: 1px;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
border-radius: 999px;
|
||||
background: var(--app-accordion-border-color);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
ul.app-search-preview {
|
||||
position: relative;
|
||||
font-size: 11px;
|
||||
padding: 0 0 0 20px;
|
||||
margin: 6px 0 1.5rem 10px;
|
||||
opacity: 0.75;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Hide preview rows (detail results) */
|
||||
.search-details-hidden ul.app-search-preview {
|
||||
display: none;
|
||||
}
|
||||
ul.app-search-preview::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 9px;
|
||||
top: 2px;
|
||||
bottom: 20px;
|
||||
width: 1px;
|
||||
background: var(--app-border);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
ul.app-search-preview li {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.app-search-preview li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -11px;
|
||||
bottom: 11px;
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
background: transparent;
|
||||
border-left: 1px solid var(--app-border);
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
ul.app-search-preview a {
|
||||
padding-inline: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
border: none;
|
||||
}
|
||||
|
||||
ul.app-search-preview a:hover {
|
||||
border: none;
|
||||
}
|
||||
|
||||
input#side-search {
|
||||
--app-form-element-spacing-horizontal: 10px;
|
||||
--app-form-element-spacing-vertical: 6px;
|
||||
height: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
ul.app-search-preview:has(li) {
|
||||
margin-bottom: 1rem!important;
|
||||
}
|
||||
|
||||
.search-result-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-reuslt-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-result-avatar,
|
||||
.search-result-avatar-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--app-border);
|
||||
background: var(--app-card-background-color);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--app-muted-color);
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.search-result-title {
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.search-result-desc {
|
||||
margin: 0;
|
||||
color: var(--app-muted-color);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.search-result-type {
|
||||
color: var(--app-muted-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
53
web/css/components/app-tabs.css
Normal file
@@ -0,0 +1,53 @@
|
||||
:root {
|
||||
--tabs-border: var(--app-border);
|
||||
--tabs-active: var(--app-primary);
|
||||
--tabs-muted: var(--app-muted-color);
|
||||
}
|
||||
|
||||
.app-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-tabs-nav {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--tabs-border);
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-block-start: 2px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.app-tabs-nav button,
|
||||
.app-tabs-nav a {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 5px 10px 5px 10px;
|
||||
color: var(--tabs-muted) !important;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
border-bottom: 2px solid transparent;
|
||||
text-decoration: none;
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.app-tabs-nav button:hover,
|
||||
.app-tabs-nav button:focus-visible,
|
||||
.app-tabs-nav a:hover,
|
||||
.app-tabs-nav a:focus-visible {
|
||||
--app-color: inherit;
|
||||
color: var(--app-color) !important;
|
||||
}
|
||||
|
||||
.app-tabs-nav button.is-active,
|
||||
.app-tabs-nav a.is-active {
|
||||
--app-color: inherit;
|
||||
color: var(--app-color) !important;
|
||||
border-color: var(--tabs-active);
|
||||
}
|
||||
|
||||
[data-tab-panel][hidden] {
|
||||
display: none;
|
||||
}
|
||||
111
web/css/components/app-tile.css
Normal file
@@ -0,0 +1,111 @@
|
||||
:root {
|
||||
--app-tile-bg: var(--app-card-background-color);
|
||||
--app-tile-border: var(--app-muted-border-color);
|
||||
--app-tile-shadow: var(--app-box-shadow);
|
||||
--app-tile-icon-bg: rgba(0, 0, 0, 0.04);
|
||||
--app-tile-icon-color: var(--app-contrast);
|
||||
--app-tile-count-color: var(--app-contrast);
|
||||
--app-tile-label-color: var(--app-color);
|
||||
--app-tile-link-color: var(--app-muted-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--app-tile-bg: rgba(20, 24, 32, 0.6);
|
||||
--app-tile-border: rgba(255, 255, 255, 0.08);
|
||||
--app-tile-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
--app-tile-icon-bg: rgba(255, 255, 255, 0.08);
|
||||
--app-tile-icon-color: #e6ecff;
|
||||
--app-tile-count-color: #f1f5ff;
|
||||
--app-tile-label-color: #d1d7e3;
|
||||
--app-tile-link-color: #98a2b3;
|
||||
}
|
||||
|
||||
|
||||
.app-tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: calc(var(--app-spacing) * 1);
|
||||
margin-bottom: calc(var(--app-spacing) * 2);
|
||||
}
|
||||
|
||||
|
||||
.app-stats-table {
|
||||
overflow: auto;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-border-radius);
|
||||
margin-bottom: calc(var(--app-spacing) * 1);
|
||||
}
|
||||
|
||||
.app-stats-table th {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.app-stats-table table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-stats-table table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.app-tile {
|
||||
--tile-icon-bg: var(--app-tile-icon-bg);
|
||||
--tile-icon-color: var(--app-tile-icon-color);
|
||||
--tile-count-color: var(--app-tile-count-color);
|
||||
--tile-label-color: var(--app-tile-label-color);
|
||||
--tile-link-color: var(--app-tile-link-color);
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: calc(var(--app-spacing) * 1);
|
||||
border: 1px solid var(--app-tile-border);
|
||||
border-radius: calc(var(--app-border-radius) * 1.1);
|
||||
background: var(--app-tile-bg);
|
||||
color: var(--app-tile-label-color);
|
||||
text-decoration: none;
|
||||
transition: transform var(--app-transition), box-shadow var(--app-transition), border-color var(--app-transition);
|
||||
}
|
||||
|
||||
.app-tile:hover {
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.app-tile-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: var(--tile-icon-bg);
|
||||
color: var(--tile-icon-color);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.app-tile-count {
|
||||
position: absolute;
|
||||
top: calc(var(--app-spacing) * 1);
|
||||
right: calc(var(--app-spacing) * 1);
|
||||
font-weight: 700;
|
||||
color: var(--tile-count-color);
|
||||
}
|
||||
|
||||
.app-tile-label {
|
||||
display: block;
|
||||
margin-top: calc(var(--app-spacing) * 1);
|
||||
font-weight: 600;
|
||||
color: var(--tile-label-color);
|
||||
}
|
||||
|
||||
.app-tile-link {
|
||||
position: absolute;
|
||||
right: calc(var(--app-spacing) * 1);
|
||||
bottom: calc(var(--app-spacing) * 1);
|
||||
color: var(--tile-link-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-tiles {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
}
|
||||
108
web/css/components/app-tooltips.css
Normal file
@@ -0,0 +1,108 @@
|
||||
:root {
|
||||
--tooltip-bg: var(--app-card-background-color);
|
||||
--tooltip-color: var(--app-color);
|
||||
--tooltip-border: var(--app-border);
|
||||
--tooltip-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
|
||||
--tooltip-radius: 8px;
|
||||
--tooltip-offset: 8px;
|
||||
}
|
||||
|
||||
[data-tooltip] {
|
||||
--app-card-background-color: inherit;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-tooltip]::after,
|
||||
[data-tooltip]::before {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
position: absolute;
|
||||
z-index: 40;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
background: var(--tooltip-bg);
|
||||
color: var(--tooltip-color);
|
||||
border: 1px solid var(--tooltip-border);
|
||||
border-radius: var(--tooltip-radius);
|
||||
box-shadow: var(--tooltip-shadow);
|
||||
font-size: 0.75rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-tooltip]::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--tooltip-bg);
|
||||
border-left: 1px solid var(--tooltip-border);
|
||||
border-top: 1px solid var(--tooltip-border);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
[data-tooltip]:hover::after,
|
||||
[data-tooltip]:hover::before,
|
||||
[data-tooltip]:focus-visible::after,
|
||||
[data-tooltip]:focus-visible::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-tooltip]::after,
|
||||
[data-tooltip]::before {
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
[data-tooltip]::after {
|
||||
transform: translate(var(--tooltip-offset), -50%);
|
||||
}
|
||||
|
||||
[data-tooltip]::before {
|
||||
transform: translate(calc(var(--tooltip-offset) - 4px), -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
[data-tooltip][data-tooltip-pos="top"]::after,
|
||||
[data-tooltip][data-tooltip-pos="center"]::after {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
top: auto;
|
||||
transform: translate(-50%, calc(-1 * var(--tooltip-offset)));
|
||||
}
|
||||
|
||||
[data-tooltip][data-tooltip-pos="top"]::before,
|
||||
[data-tooltip][data-tooltip-pos="center"]::before {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
top: auto;
|
||||
transform: translate(-50%, calc(-1 * (var(--tooltip-offset) - 2px))) rotate(45deg);
|
||||
}
|
||||
|
||||
[data-tooltip][data-tooltip-pos="bottom"]::after {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, var(--tooltip-offset));
|
||||
}
|
||||
|
||||
[data-tooltip][data-tooltip-pos="bottom"]::before {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, calc(var(--tooltip-offset) - 2px)) rotate(45deg);
|
||||
}
|
||||
|
||||
[data-tooltip][data-tooltip-pos="left"]::after {
|
||||
top: 50%;
|
||||
right: 100%;
|
||||
left: auto;
|
||||
transform: translate(calc(-1 * var(--tooltip-offset)), -50%);
|
||||
}
|
||||
|
||||
[data-tooltip][data-tooltip-pos="left"]::before {
|
||||
top: 50%;
|
||||
right: 100%;
|
||||
left: auto;
|
||||
transform: translate(calc(-1 * (var(--tooltip-offset) - 2px)), -50%) rotate(45deg);
|
||||
}
|
||||
74
web/css/components/vendor-editorjs.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.codex-editor {
|
||||
font-family: var(--app-font-family);
|
||||
color: var(--app-color);
|
||||
}
|
||||
|
||||
.ce-block__content {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.ce-toolbar__content,
|
||||
.ce-inline-toolbar {
|
||||
border-radius: var(--app-border-radius);
|
||||
}
|
||||
|
||||
.ce-inline-toolbar {
|
||||
box-shadow: var(--app-box-shadow);
|
||||
}
|
||||
|
||||
.ce-toolbar__content {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
input.cdx-search-field__input {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
height: auto;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.cdx-search-field__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ce-popover__items button {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: inherit;
|
||||
border: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input.ce-inline-tool-input.ce-inline-tool-input--showed {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table.tc-table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tc-cell {
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ce-toolbar__actions {
|
||||
right: calc(100% + 10px)!important;
|
||||
background-color: white!important;
|
||||
}
|
||||
|
||||
.ce-block--selected .ce-block__content {
|
||||
background: var(--app-table-row-stripped-background-color);
|
||||
}
|
||||
|
||||
.ce-inline-tool svg {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.ce-popover--inline .ce-popover-item__icon svg {
|
||||
color: black;
|
||||
}
|
||||
207
web/css/components/vendor-gridjs.css
Normal file
@@ -0,0 +1,207 @@
|
||||
.gridjs-container {
|
||||
color: var(--app-color);
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gridjs-wrapper {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background-color: var(--app-background-color);
|
||||
border-top: var(--app-border-width) solid var(--app-table-border-color);
|
||||
padding-block-end: 1rem;
|
||||
min-height: 515px;
|
||||
}
|
||||
|
||||
.gridjs-footer {
|
||||
background-color: var(--app-background-color);
|
||||
border: 0;
|
||||
border-top: 0;
|
||||
box-shadow: none;
|
||||
color: var(--app-muted-color);
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
|
||||
.gridjs-table {
|
||||
color: var(--app-color);
|
||||
}
|
||||
|
||||
th.gridjs-th {
|
||||
padding: calc(var(--app-spacing) / 2) var(--app-spacing);
|
||||
border: none;
|
||||
border-bottom: var(--app-border-width) solid var(--app-table-border-color);
|
||||
background-color: var(--app-background-color);
|
||||
color: var(--app-color);
|
||||
font-weight: var(--app-font-weight);
|
||||
text-align: left;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
button.gridjs-sort {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
th.gridjs-th-sort:hover,
|
||||
th.gridjs-th-sort:focus {
|
||||
background-color: var(--app-table-row-stripped-background-color);
|
||||
}
|
||||
|
||||
td.gridjs-td {
|
||||
background-color: var(--app-background-color);
|
||||
border: none;
|
||||
border-color: var(--app-table-border-color);
|
||||
padding: calc(var(--app-spacing) / 2) var(--app-spacing);
|
||||
border-bottom: var(--app-border-width) solid var(--app-table-border-color);
|
||||
background-color: var(--app-background-color);
|
||||
color: var(--app-color);
|
||||
font-weight: var(--app-font-weight);
|
||||
text-align: left;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.gridjs-search {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.gridjs-search-input {
|
||||
width: min(320px, 100%);
|
||||
}
|
||||
|
||||
input.gridjs-input {
|
||||
background-color: var(--app-background-color);
|
||||
border: var(--app-border-width) solid var(--app-form-element-border-color);
|
||||
border-radius: var(--app-border-radius);
|
||||
color: var(--app-color);
|
||||
font-size: 0.95rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
input.gridjs-input:focus {
|
||||
border-color: var(--app-primary);
|
||||
box-shadow: 0 0 0 var(--app-outline-width) var(--app-primary-focus);
|
||||
}
|
||||
|
||||
.gridjs-pagination {
|
||||
color: var(--app-muted-color);
|
||||
}
|
||||
|
||||
.gridjs-pagination .gridjs-pages button {
|
||||
background-color: var(--app-background-color);
|
||||
border: var(--app-border-width) solid var(--app-table-border-color);
|
||||
color: var(--app-color);
|
||||
padding: 0.4rem 0.7rem;
|
||||
}
|
||||
|
||||
.gridjs-pagination .gridjs-pages button:hover {
|
||||
background-color: var(--app-primary-hover-background);
|
||||
border-color: var(--app-primary-hover-border);
|
||||
color: var(--app-primary-inverse);
|
||||
}
|
||||
|
||||
.gridjs-pagination .gridjs-pages button:focus {
|
||||
box-shadow: 0 0 0 var(--app-outline-width) var(--app-primary-focus);
|
||||
}
|
||||
|
||||
.gridjs-pagination .gridjs-pages button.gridjs-currentPage {
|
||||
background-color: var(--app-primary) !important;
|
||||
font-weight: 400;
|
||||
padding: 2px 10px;
|
||||
color: var(--app-secondary-inverse) !important;
|
||||
}
|
||||
|
||||
.gridjs-pagination .gridjs-pages button:disabled, .gridjs-pagination .gridjs-pages button[disabled] {
|
||||
background-color: var(--app-background-color);
|
||||
color: var(--app-muted-color);
|
||||
cursor: default;
|
||||
padding: 2px 10px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.gridjs-pagination .gridjs-pages {
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.gridjs-loading-bar {
|
||||
background-color: var(--app-background-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.grid-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--app-muted-border-color);
|
||||
background: var(--app-card-background-color);
|
||||
object-fit: contain;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.grid-avatar-placeholder {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
color: var(--app-muted-color);
|
||||
font-size: 11px;
|
||||
background: var(--app-background-color);
|
||||
}
|
||||
|
||||
.gridjs-loading-bar:after {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
var(--app-primary-focus),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
/* .gridjs-container:not(.gridjs-has-loaded) .gridjs-message.gridjs-notfound {
|
||||
display: none;
|
||||
} */
|
||||
|
||||
.gridjs-container.gridjs-is-updating .gridjs-message.gridjs-notfound {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gridjs-pages button {
|
||||
background: var(--app-form-element-background-color) !important;
|
||||
color: var(--app-contrast) !important;
|
||||
}
|
||||
|
||||
.gridjs-summary {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
tr.gridjs-tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr.gridjs-tr:hover td {
|
||||
background: var(--app-table-row-stripped-background-color);
|
||||
}
|
||||
|
||||
html[data-theme=dark] button.gridjs-sort,
|
||||
html[data-theme=dark] button.gridjs-sort-neutral {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.gridjs-tbody, td.gridjs-td {
|
||||
background-color: var(--app-background-color)
|
||||
}
|
||||
|
||||
.gridjs-table input[data-grid-select-all] {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
border-width: .5px;
|
||||
}
|
||||
|
||||
.gridjs-th-content:has(input[data-grid-select-all]) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gridjs-pagination .gridjs-pages button:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
130
web/css/components/vendor-multi-select.css
Normal file
@@ -0,0 +1,130 @@
|
||||
|
||||
:root {
|
||||
--spacing-smaller: 3px;
|
||||
--spacing-small: 5px;
|
||||
--spacing-medium: 7px;
|
||||
--spacing-large: 12px;
|
||||
--font-size: 12px;
|
||||
--font-size-large: 14px;
|
||||
--font-size-larger: 16px;
|
||||
--line-height: 16px;
|
||||
--line-height-larger: 20px;
|
||||
--primary-color: var(--app-primary);
|
||||
--text-color-dark: #212529;
|
||||
--text-color: #585858;
|
||||
--text-color-light: #65727e;
|
||||
--border-color: #bebebe;
|
||||
--border-color-light: #f1f3f5;
|
||||
--input-placeholder: #65727e;
|
||||
--input-background: #e9e9ed;
|
||||
--input-border: #dee2e6;
|
||||
--input-border-active: #c1c9d0;
|
||||
--input-border-invalid: #e44e4e;
|
||||
--input-outline-invalid: rgba(219, 138, 138, 0.5);
|
||||
--input-color: #e9e9ed;
|
||||
--input-disabled: #f7f7f7;
|
||||
--input-min-height: 45px;
|
||||
--options-height: 40dvh;
|
||||
--option-background: #f3f4f7;
|
||||
--border-radius: 5px;
|
||||
--icon-size: 12px;
|
||||
--icon-space: 30px;
|
||||
--checkbox-size: 16px;
|
||||
--checkbox-radius: 4px;
|
||||
--checkbox-border: #ced4da;
|
||||
--checkbox-background: #fff;
|
||||
--checkbox-active: #fff;
|
||||
--checkbox-thickness: 2px;
|
||||
}
|
||||
/* Overrides for MultiSelect */
|
||||
.multi-select {
|
||||
--spacing-smaller: 3px;
|
||||
--spacing-small: 5px;
|
||||
--spacing-medium: 7px;
|
||||
--spacing-large: 12px;
|
||||
--font-size: 12px;
|
||||
--font-size-large: 14px;
|
||||
--font-size-larger: 16px;
|
||||
--line-height: 16px;
|
||||
--line-height-larger: 20px;
|
||||
--primary-color: var(--app-primary);
|
||||
--text-color-dark: var(--app-contrast);
|
||||
--text-color: var(--app-muted-color);
|
||||
--text-color-light: var(--app-muted-color);
|
||||
--border-color: var(--app-form-element-border-color);
|
||||
--border-color-light: var(--app-muted-border-color);
|
||||
--input-placeholder: var(--app-form-element-placeholder-color);
|
||||
--input-background: var(--app-form-element-background-color);
|
||||
--input-border: var(--app-form-element-border-color);
|
||||
--input-border-active: var(--app-primary-focus);
|
||||
--input-border-invalid: var(--app-form-element-invalid-border-color);
|
||||
--input-outline-invalid: var(--app-form-element-invalid-active-shadow);
|
||||
--input-color: var(--app-form-element-color);
|
||||
--input-disabled: var(--app-form-element-disabled-background-color);
|
||||
--input-min-height: calc(
|
||||
1rem * var(--app-line-height) + var(--app-form-element-spacing-vertical) *
|
||||
2 + var(--app-border-width) * 2
|
||||
);
|
||||
--options-height: 40dvh;
|
||||
--option-background: var(--badge-neutral-bg);
|
||||
--border-radius: var(--app-border-radius);
|
||||
--icon-size: 12px;
|
||||
--icon-space: 30px;
|
||||
--checkbox-size: 16px;
|
||||
--checkbox-radius: 4px;
|
||||
--checkbox-border: var(--app-form-element-border-color);
|
||||
--checkbox-background: var(--app-form-element-background-color);
|
||||
--checkbox-active: var(--app-form-element-background-color);
|
||||
--checkbox-thickness: 2px;
|
||||
}
|
||||
|
||||
.multi-select
|
||||
.multi-select-options
|
||||
.multi-select-option
|
||||
.multi-select-option-radio,
|
||||
.multi-select
|
||||
.multi-select-options
|
||||
.multi-select-all
|
||||
.multi-select-option-radio {
|
||||
padding: 2px;
|
||||
}
|
||||
.multi-select
|
||||
.multi-select-options
|
||||
.multi-select-option
|
||||
.multi-select-option-radio,
|
||||
.multi-select
|
||||
.multi-select-options
|
||||
.multi-select-all
|
||||
.multi-select-option-radio {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.multi-select {
|
||||
margin-bottom: var(--app-spacing);
|
||||
}
|
||||
|
||||
.multi-select.disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.multi-select.disabled .multi-select-header,
|
||||
.multi-select.disabled .multi-select-options {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.multi-select .multi-select-header {
|
||||
background: var(--app-form-element-background-color);
|
||||
}
|
||||
|
||||
.multi-select .multi-select-options {
|
||||
background: var(--app-form-element-background-color);
|
||||
}
|
||||
|
||||
.multi-select .multi-select-header .multi-select-header-option {
|
||||
border: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.multi-select .multi-select-header {
|
||||
padding-left: 7px;
|
||||
}
|
||||
60
web/css/layout/app-aside-icon-bar.css
Normal file
@@ -0,0 +1,60 @@
|
||||
aside.aside-icon-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
aside.aside-icon-bar ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
aside.aside-icon-bar li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
aside.aside-icon-bar li a,
|
||||
aside.aside-icon-bar li button {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
color: var(--app-muted-color);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-left: 3px solid transparent;
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
aside.aside-icon-bar li a:hover, aside.aside-icon-bar li a.active, aside.aside-icon-bar li button:hover, aside.aside-icon-bar li button.active {
|
||||
border-left: 3px solid var(--app-primary);
|
||||
color: var(--app-contrast);
|
||||
background: var(--app-accordion-border-color);
|
||||
}
|
||||
|
||||
aside.aside-icon-bar nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: calc(100vh - var(--app-spacing));
|
||||
}
|
||||
|
||||
/* aside.aside-icon-bar ul.aside-icon-group li:first-child {
|
||||
margin-bottom: 1rem;
|
||||
} */
|
||||
|
||||
@media (min-width: 768px) {
|
||||
aside.aside-icon-bar {
|
||||
border-right: 1px solid var(--app-border);
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 11;
|
||||
padding-top: var(--app-spacing);
|
||||
}
|
||||
.is-sidebar-collapsed aside.aside-icon-bar {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
2978
web/css/layout/app-shell.css
Normal file
208
web/css/layout/app-sidebar.css
Normal file
@@ -0,0 +1,208 @@
|
||||
.app-sidebar {
|
||||
background: var(--app-sidebar-background-color);
|
||||
}
|
||||
|
||||
.app-sidebar .brand {
|
||||
padding: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-sidebar-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0px var(--app-spacing) 0px var(--app-spacing);
|
||||
margin-bottom: 0;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-panel {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-panel[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.app-sidebar.is-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .app-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-sidebar-tenant-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
padding: var(--app-spacing) var(--app-spacing) 10px var(--app-spacing);
|
||||
}
|
||||
|
||||
|
||||
.app-sidebar .app-sidebar-tenant-logo a:hover {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-tenant-logo > a {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.app-sidebar-tenant-name {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--app-contrast);
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--app-border-radius);
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.app-sidebar-title {
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
color: var(--app-muted-color);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-sidebar-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.app-sidebar-tools .icon-button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--app-muted-color);
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.app-sidebar-tools .icon-button:hover {
|
||||
color: var(--app-contrast);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.app-sidebar .brand {
|
||||
margin-bottom: 1rem;
|
||||
padding-block-start: 0.5rem;
|
||||
}
|
||||
.app-sidebar-title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.app-sidebar a {
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
padding: 5px var(--app-spacing);
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border-left: 2px solid transparent;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-sidebar a:hover,
|
||||
.app-sidebar .active {
|
||||
color: var(--app-primary);
|
||||
--app-background-color: inherit;
|
||||
background: var(--app-background-color);
|
||||
border-left: 2px solid var(--app-primary);
|
||||
}
|
||||
|
||||
aside.app-sidebar ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-sidebar small {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
padding-inline: var(--app-spacing);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.app-sidebar .app-sidebar-group small {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
display: block;
|
||||
padding-inline: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.app-sidebar li {
|
||||
padding-block: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group details {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group summary {
|
||||
padding: 0 var(--app-spacing);
|
||||
font-size: 12px;
|
||||
color: var(--app-muted-color);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
nav#aside-panel-people {
|
||||
margin-top: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group details > ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group summary > span {
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
max-width: 157px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group details > ul li:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group details > ul li a {
|
||||
padding-block: 2px;
|
||||
opacity: .7;
|
||||
}
|
||||
.app-sidebar .app-sidebar-group details > ul li a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
87
web/css/layout/app-topbar.css
Normal file
@@ -0,0 +1,87 @@
|
||||
.app-header nav li {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form:has(input#app-search) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-header nav {
|
||||
gap: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
padding-block-start: 0;
|
||||
padding-left: var(--app-spacing);
|
||||
padding-right: var(--app-spacing);
|
||||
margin-bottom: calc(var(--app-spacing) * 2);
|
||||
color: var(--app-contrast);
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
main:has(.app-details-container) .app-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li:has(#toggle-main-content-aside) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
main:has(#app-details-aside-section) li:has(#toggle-main-content-aside) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-header nav > ul {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--app-muted-color);
|
||||
}
|
||||
|
||||
.app-header details.dropdown > summary:not([role]) {
|
||||
border: none;
|
||||
height: auto;
|
||||
padding: 5px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.app-header details.dropdown > summary:not([role]):after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-header nav > ul li a {
|
||||
padding: 5px;
|
||||
color: var(--app-form-element-placeholder-color);
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-header button {
|
||||
padding: 5px;
|
||||
color: var(--app-form-element-placeholder-color);
|
||||
display: block;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.app-header #global-back.is-disabled,
|
||||
.app-header #global-forward.is-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
header.app-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--app-background-color);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.app-header details.dropdown[open] > summary::before {
|
||||
display: none;
|
||||
}
|
||||
10
web/css/pages/app-login.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.back_to_login {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
main {
|
||||
min-height: 90vh;
|
||||
place-content: center;
|
||||
}
|
||||
}
|
||||
BIN
web/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
web/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 451 B |
BIN
web/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 816 B |
BIN
web/favicon/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
web/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
36
web/favicon/favicon.svg
Normal file
@@ -0,0 +1,36 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="146.51" height="121.08"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="146.51" height="121.08"><svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 146.51 121.08">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #71ab51;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #1e7729;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #3d8c33;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #c4d6a4;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #69af67;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="cls-4" d="M123.06,30.78L103.94,3.63c-1.41-2.01-4.54-3.63-6.99-3.63h-47.39c-1.18,0-2.53.39-3.76,1.01l20.06,29.78h57.2Z"></path>
|
||||
<path class="cls-2" d="M90.2,64.39l-39.69,56.69h91.56c2.45,0,4.44-1.99,4.44-4.44v-47.82c0-1.29-.32-2.9-.83-4.44h-55.49Z"></path>
|
||||
<path class="cls-1" d="M45.8,1.01c-1.31.66-2.5,1.59-3.23,2.62L2.55,60.47c-1.41,2-2.55,5.62-2.55,8.07v48.1c0,1.12.43,2.13,1.11,2.91L65.86,30.78,45.8,1.01Z"></path>
|
||||
<path class="cls-3" d="M65.86,30.78L1.11,119.56c.81.93,1.99,1.53,3.33,1.53h46.07l39.69-56.69-24.34-33.61Z"></path>
|
||||
<path class="cls-5" d="M90.2,64.39h55.49c-.45-1.37-1.05-2.69-1.71-3.64l-20.92-29.97h-57.2l24.34,33.61Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||
@media (prefers-color-scheme: dark) { :root { filter: none; } }
|
||||
</style></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
21
web/favicon/site.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "IVMS",
|
||||
"short_name": "IVMS",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicon/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 521 B |
|
After Width: | Height: | Size: 1005 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
BIN
web/favicon/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
web/favicon/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
web/img/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
web/img/forkme_right_red_aa0000.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
web/img/minty_square.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
web/img/mintyphp_logo.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
159
web/index.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Firewall;
|
||||
use MintyPHP\Session;
|
||||
use MintyPHP\Debugger;
|
||||
use MintyPHP\Analyzer;
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Http\Request;
|
||||
use MintyPHP\Http\LocaleResolver;
|
||||
use MintyPHP\Http\AccessControl;
|
||||
use MintyPHP\Http\AssetDetector;
|
||||
|
||||
// Change directory to project root
|
||||
chdir(__DIR__ . '/..');
|
||||
// Load the libraries
|
||||
require 'vendor/autoload.php';
|
||||
// Load the config parameters
|
||||
require 'config/config.php';
|
||||
// Load the routes
|
||||
require 'config/router.php';
|
||||
// Register shortcut functions
|
||||
require 'lib/Support/helpers.php';
|
||||
|
||||
// Start the firewall
|
||||
Firewall::start();
|
||||
|
||||
// Start the session
|
||||
Session::start();
|
||||
|
||||
// Initialize default locale from tenant/app settings
|
||||
$defaultLocale = appDefaultLocale();
|
||||
if ($defaultLocale !== null) {
|
||||
I18n::$defaultLocale = $defaultLocale;
|
||||
if (!defined('APP_LOCALES') || in_array($defaultLocale, APP_LOCALES, true)) {
|
||||
if (I18n::$locale === '' || !in_array(I18n::$locale, APP_LOCALES, true)) {
|
||||
I18n::$locale = $defaultLocale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-login from remember-me cookie
|
||||
if (empty($_SESSION['user']['id'])) {
|
||||
\MintyPHP\Service\RememberMeService::autoLoginFromCookie();
|
||||
}
|
||||
|
||||
// Parse request URI and resolve locale
|
||||
$originalUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
$_SERVER['MINTY_ORIGINAL_URI'] = $originalUri;
|
||||
|
||||
$localeResolver = new LocaleResolver();
|
||||
$parsedUri = $localeResolver->parseUri($originalUri);
|
||||
$pathWithoutLocale = $parsedUri['pathWithoutLocale'];
|
||||
$requestedLocale = $parsedUri['locale'];
|
||||
$_SERVER['MINTY_LOCALE'] = $requestedLocale;
|
||||
|
||||
// Rewrite REQUEST_URI without locale segment for router
|
||||
if ($parsedUri['hadLocaleInUrl']) {
|
||||
$_SERVER['REQUEST_URI'] = $localeResolver->buildUriWithoutLocale(
|
||||
$pathWithoutLocale,
|
||||
$parsedUri['query']
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve effective locale from URL > Session > Cookie > Default
|
||||
I18n::$locale = $localeResolver->resolveLocale(
|
||||
$requestedLocale,
|
||||
$_SESSION['user']['locale'] ?? null,
|
||||
$_COOKIE['locale'] ?? null
|
||||
);
|
||||
|
||||
$isAssetRequest = AssetDetector::isAssetRequest($pathWithoutLocale);
|
||||
|
||||
// Refresh tenant context if stale or missing (skip for assets)
|
||||
if (!empty($_SESSION['user']['id']) && !$isAssetRequest) {
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId > 0) {
|
||||
$ttl = defined('TENANT_SNAPSHOT_TTL') ? (int) TENANT_SNAPSHOT_TTL : 120;
|
||||
$lastRefresh = (int) ($_SESSION['tenant_snapshot_at'] ?? 0);
|
||||
$needsRefresh = empty($_SESSION['current_tenant']) || empty($_SESSION['available_tenants']);
|
||||
if ($ttl > 0 && (time() - $lastRefresh) >= $ttl) {
|
||||
$needsRefresh = true;
|
||||
}
|
||||
if ($needsRefresh) {
|
||||
\MintyPHP\Service\AuthService::loadTenantDataIntoSession($userId);
|
||||
$_SESSION['tenant_snapshot_at'] = time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($requestedLocale === '' && !$isAssetRequest) {
|
||||
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||
if (in_array($method, ['GET', 'HEAD'], true)) {
|
||||
$target = Request::withLocale($pathWithoutLocale, I18n::$locale ?? I18n::$defaultLocale);
|
||||
$target .= $parsedUri['query'] !== '' ? '?' . $parsedUri['query'] : '';
|
||||
Router::redirect($target);
|
||||
}
|
||||
}
|
||||
|
||||
// Guard: restrict non-public pages to logged-in users
|
||||
$accessControl = new AccessControl();
|
||||
$accessControl->requireAuthOrRedirect($pathWithoutLocale, !empty($_SESSION['user']['id']));
|
||||
|
||||
// Analyze the PHP code
|
||||
if (Debugger::$enabled) {
|
||||
Analyzer::execute();
|
||||
}
|
||||
|
||||
// Load the action into body
|
||||
ob_start();
|
||||
if (Router::getTemplateAction()) {
|
||||
require Router::getTemplateAction();
|
||||
}
|
||||
if (ob_get_contents()) {
|
||||
ob_end_flush();
|
||||
trigger_error('MintyPHP template action"' . Router::getTemplateAction() . '" should not send output. Error raised ', E_USER_WARNING);
|
||||
} else {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
ob_start();
|
||||
if (Router::getAction()) {
|
||||
extract(Router::getParameters());
|
||||
require Router::getAction();
|
||||
}
|
||||
if (ob_get_contents()) {
|
||||
ob_end_flush();
|
||||
trigger_error('MintyPHP action "' . Router::getAction() . '" should not send output. Error raised ', E_USER_WARNING);
|
||||
} else {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// End the session
|
||||
Session::end();
|
||||
|
||||
// Close the database connection
|
||||
DB::close();
|
||||
|
||||
if (Router::getTemplateView()) {
|
||||
Buffer::start('html');
|
||||
if (Router::getView()) {
|
||||
require Router::getView();
|
||||
}
|
||||
|
||||
// Show developer toolbar
|
||||
if (Debugger::$enabled) {
|
||||
Debugger::toolbar();
|
||||
}
|
||||
|
||||
Buffer::end('html');
|
||||
// Load body into template
|
||||
require Router::getTemplateView();
|
||||
} else { // Handle the 'none' template case
|
||||
if (Router::getView()) {
|
||||
require Router::getView();
|
||||
}
|
||||
}
|
||||
21
web/js/app-init.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import './components/app-flash-auto-dismiss.js';
|
||||
import './components/app-fslightbox-refresh.js';
|
||||
import './components/app-password-hints.js';
|
||||
import './components/app-copy-badge.js';
|
||||
import './components/app-multiselect-init.js';
|
||||
import './components/app-nav-history.js';
|
||||
import './components/app-toggle-sidebar.js';
|
||||
import './components/app-toggle-aside-panels.js';
|
||||
import './components/app-toggle-details-aside.js';
|
||||
import './components/app-global-search.js';
|
||||
import './components/app-theme-toggle.js';
|
||||
import './components/app-tenant-switcher.js';
|
||||
import './components/app-tabs.js';
|
||||
import './components/app-color-default-toggle.js';
|
||||
|
||||
// Ensure fslightbox is refreshed once the vendor script is loaded.
|
||||
if (typeof window.refreshFsLightbox === 'function') {
|
||||
window.refreshFsLightbox();
|
||||
} else {
|
||||
window.__fsLightboxRefreshPending = true;
|
||||
}
|
||||
2
web/js/app-login-init.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import './components/app-flash-auto-dismiss.js';
|
||||
import './components/app-password-hints.js';
|
||||
89
web/js/components/app-async-flash.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Async Flash Messages & Loading State
|
||||
* Renders flash messages into #async-messages for JS-triggered notifications
|
||||
* Provides loading cursor state management
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set global loading state (cursor + pointer-events)
|
||||
* @param {boolean} loading - Whether to show loading state
|
||||
*/
|
||||
export function setLoading(loading) {
|
||||
if (loading) {
|
||||
document.body.classList.add('is-loading');
|
||||
} else {
|
||||
document.body.classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an async function with loading state
|
||||
* @param {Function} fn - Async function to execute
|
||||
* @returns {Promise<any>} - Result of the function
|
||||
*/
|
||||
export async function withLoading(fn) {
|
||||
setLoading(true);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTimeouts = {
|
||||
success: 10000,
|
||||
info: 10000,
|
||||
warning: 10000,
|
||||
error: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Show an async flash message
|
||||
* @param {string} type - Message type: 'success', 'info', 'warning', 'error'
|
||||
* @param {string} message - The message text
|
||||
* @param {number} [timeout] - Auto-dismiss timeout in ms (0 = no auto-dismiss)
|
||||
*/
|
||||
export function showAsyncFlash(type, message, timeout) {
|
||||
const container = document.getElementById('async-messages');
|
||||
if (!container) {
|
||||
console.warn('async-messages container not found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const effectiveTimeout = timeout ?? defaultTimeouts[type] ?? 5000;
|
||||
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'notice';
|
||||
notice.setAttribute('data-variant', type);
|
||||
|
||||
const messageSpan = document.createElement('span');
|
||||
messageSpan.textContent = message;
|
||||
notice.appendChild(messageSpan);
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.type = 'button';
|
||||
closeButton.className = 'contrast outline small';
|
||||
closeButton.innerHTML = '<i class="bi bi-x"></i>';
|
||||
closeButton.addEventListener('click', () => notice.remove());
|
||||
notice.appendChild(closeButton);
|
||||
|
||||
container.appendChild(notice);
|
||||
|
||||
if (effectiveTimeout > 0) {
|
||||
notice.style.setProperty('--flash-timeout', `${effectiveTimeout}ms`);
|
||||
notice.classList.add('flash-timed');
|
||||
setTimeout(() => notice.remove(), effectiveTimeout);
|
||||
}
|
||||
|
||||
return notice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all async flash messages
|
||||
*/
|
||||
export function clearAsyncFlash() {
|
||||
const container = document.getElementById('async-messages');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
26
web/js/components/app-bulk-selection.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export function bindBulkVisibility({
|
||||
containerSelector,
|
||||
buttonSelector,
|
||||
selectedRowSelector = '.gridjs-tr-selected'
|
||||
} = {}) {
|
||||
const container = document.querySelector(containerSelector);
|
||||
const buttons = Array.from(document.querySelectorAll(buttonSelector));
|
||||
if (!container || !buttons.length) return { update: () => {} };
|
||||
|
||||
const update = () => {
|
||||
const selected = container.querySelectorAll(selectedRowSelector).length > 0;
|
||||
buttons.forEach((button) => {
|
||||
button.hidden = !selected;
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(() => update());
|
||||
observer.observe(container, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
|
||||
update();
|
||||
return { update, destroy: () => observer.disconnect() };
|
||||
}
|
||||
43
web/js/components/app-color-default-toggle.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const toggles = document.querySelectorAll('[data-color-default-toggle]');
|
||||
|
||||
const syncToggle = (toggle) => {
|
||||
const targetSelector = toggle.dataset.colorTarget || '';
|
||||
if (!targetSelector) {
|
||||
return;
|
||||
}
|
||||
const target = document.querySelector(targetSelector);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const defaultValue = toggle.dataset.colorDefault || '#2fa4a4';
|
||||
if (toggle.checked) {
|
||||
if (!target.dataset.customValue) {
|
||||
target.dataset.customValue = target.value;
|
||||
}
|
||||
target.value = defaultValue;
|
||||
target.setAttribute('disabled', 'disabled');
|
||||
} else {
|
||||
target.removeAttribute('disabled');
|
||||
if (target.dataset.customValue) {
|
||||
target.value = target.dataset.customValue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toggles.forEach((toggle) => {
|
||||
const targetSelector = toggle.dataset.colorTarget || '';
|
||||
const target = targetSelector ? document.querySelector(targetSelector) : null;
|
||||
|
||||
toggle.addEventListener('change', () => syncToggle(toggle));
|
||||
|
||||
if (target) {
|
||||
target.addEventListener('input', () => {
|
||||
if (toggle.checked) {
|
||||
toggle.checked = false;
|
||||
target.removeAttribute('disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
syncToggle(toggle);
|
||||
});
|
||||
66
web/js/components/app-copy-badge.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const COPY_SELECTOR = '.badge[data-copy="true"]';
|
||||
const COPIED_CLASS = 'is-copied';
|
||||
|
||||
const getCopyValue = (badge) => {
|
||||
const explicit = badge.getAttribute('data-copy-value');
|
||||
if (explicit !== null && explicit !== '') {
|
||||
return explicit;
|
||||
}
|
||||
return (badge.textContent || '').trim();
|
||||
};
|
||||
|
||||
const copyText = async (value) => {
|
||||
if (!value) return false;
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
}
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = value;
|
||||
textarea.setAttribute('readonly', 'true');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
return ok;
|
||||
};
|
||||
|
||||
const enhanceBadge = (badge) => {
|
||||
if (badge.querySelector('.badge-copy-icon')) return;
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'badge-copy-icon';
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
icon.innerHTML = '<i class="bi bi-copy"></i><i class="bi bi-check2"></i>';
|
||||
badge.appendChild(icon);
|
||||
badge.setAttribute('role', 'button');
|
||||
badge.setAttribute('tabindex', '0');
|
||||
if (!badge.hasAttribute('aria-label')) {
|
||||
badge.setAttribute('aria-label', 'Copy to clipboard');
|
||||
}
|
||||
const handler = async (event) => {
|
||||
event.preventDefault();
|
||||
const value = getCopyValue(badge);
|
||||
const ok = await copyText(value);
|
||||
if (!ok) return;
|
||||
badge.classList.add(COPIED_CLASS);
|
||||
window.setTimeout(() => badge.classList.remove(COPIED_CLASS), 1200);
|
||||
};
|
||||
badge.addEventListener('click', handler);
|
||||
badge.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
handler(event);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const initBadgeCopy = () => {
|
||||
document.querySelectorAll(COPY_SELECTOR).forEach(enhanceBadge);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBadgeCopy);
|
||||
} else {
|
||||
initBadgeCopy();
|
||||
}
|
||||
53
web/js/components/app-flash-auto-dismiss.js
Normal file
@@ -0,0 +1,53 @@
|
||||
export function initFlashAutoDismiss(options = {}) {
|
||||
const {
|
||||
selector = '.flash-stack .notice[data-flash-timeout]',
|
||||
defaultTimeout = 0
|
||||
} = options;
|
||||
|
||||
const notices = document.querySelectorAll(selector);
|
||||
if (!notices.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postForm = async (form) => {
|
||||
const action = form.getAttribute('action');
|
||||
if (!action) return null;
|
||||
const body = new URLSearchParams(new FormData(form));
|
||||
return fetch(action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'fetch'
|
||||
},
|
||||
body
|
||||
});
|
||||
};
|
||||
|
||||
notices.forEach((notice) => {
|
||||
const timeout = Number.parseInt(
|
||||
notice.dataset.flashTimeout || `${defaultTimeout}`,
|
||||
10
|
||||
);
|
||||
if (!timeout || timeout <= 0) {
|
||||
return;
|
||||
}
|
||||
notice.style.setProperty('--flash-timeout', `${timeout}ms`);
|
||||
notice.classList.add('flash-timed');
|
||||
window.setTimeout(async () => {
|
||||
const form = notice.querySelector('form');
|
||||
if (form) {
|
||||
const response = await postForm(form);
|
||||
if (!response || !response.ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
notice.remove();
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initFlashAutoDismiss());
|
||||
} else {
|
||||
initFlashAutoDismiss();
|
||||
}
|
||||
48
web/js/components/app-fslightbox-refresh.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const refreshFsLightbox = () => {
|
||||
if (typeof window.refreshFsLightbox === 'function') {
|
||||
window.refreshFsLightbox();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let refreshTimer = null;
|
||||
const scheduleRefresh = () => {
|
||||
if (refreshTimer) return;
|
||||
refreshTimer = window.setTimeout(() => {
|
||||
refreshTimer = null;
|
||||
refreshFsLightbox();
|
||||
}, 30);
|
||||
};
|
||||
|
||||
const initObserver = () => {
|
||||
if (!document.body || typeof MutationObserver === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (!(node instanceof HTMLElement)) continue;
|
||||
if (node.matches?.('[data-fslightbox]') || node.querySelector?.('[data-fslightbox]')) {
|
||||
scheduleRefresh();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
};
|
||||
|
||||
if (!refreshFsLightbox()) {
|
||||
scheduleRefresh();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshFsLightbox();
|
||||
initObserver();
|
||||
});
|
||||
} else {
|
||||
refreshFsLightbox();
|
||||
initObserver();
|
||||
}
|
||||
168
web/js/components/app-global-search.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const input = document.querySelector('#side-search');
|
||||
const resultsEl = document.querySelector('[data-global-search-results]');
|
||||
|
||||
if (!input || !resultsEl) {
|
||||
if (!input) console.warn('[GlobalSearch] #side-search input not found');
|
||||
if (!resultsEl) console.warn('[GlobalSearch] [data-global-search-results] container not found');
|
||||
} else {
|
||||
const panel = document.querySelector('#aside-panel-search');
|
||||
const toggleButton = document.querySelector('[data-search-details-toggle]');
|
||||
const toggleIcon = document.querySelector('[data-search-details-icon]');
|
||||
const appBase = window.APP_BASE || document.baseURI;
|
||||
const endpoint = new URL('admin/search/data', appBase);
|
||||
const resultsPage = new URL('search', appBase);
|
||||
const minLength = 2;
|
||||
let pending = null;
|
||||
|
||||
const itemsByKey = new Map();
|
||||
resultsEl.querySelectorAll('[data-search-key]').forEach((item) => {
|
||||
const key = item.getAttribute('data-search-key');
|
||||
if (key) itemsByKey.set(key, item);
|
||||
});
|
||||
if (itemsByKey.size === 0) {
|
||||
console.warn('[GlobalSearch] No [data-search-key] elements found in results container');
|
||||
}
|
||||
|
||||
const setLoadingState = (value) => {
|
||||
resultsEl.querySelectorAll('[data-search-count]').forEach((el) => {
|
||||
el.textContent = value;
|
||||
});
|
||||
};
|
||||
|
||||
const render = (items, query) => {
|
||||
items.forEach((item) => {
|
||||
const li = itemsByKey.get(item.key);
|
||||
if (!li) {
|
||||
console.warn('[GlobalSearch] No element found for key:', item.key);
|
||||
return;
|
||||
}
|
||||
const countEl = li.querySelector('[data-search-count]');
|
||||
const link = li.querySelector('a');
|
||||
const previewEl = li.querySelector('[data-search-preview]');
|
||||
const base = item.key === 'pages' && item.url ? item.url : (li.getAttribute('data-search-base') || item.url);
|
||||
const url = new URL(base, appBase);
|
||||
if (item.key !== 'pages') {
|
||||
url.searchParams.set('search', query);
|
||||
}
|
||||
if (countEl) countEl.textContent = item.count.toString();
|
||||
if (link) {
|
||||
link.href = item.count > 0 ? url.toString() : base;
|
||||
link.setAttribute('aria-disabled', item.count > 0 ? 'false' : 'true');
|
||||
}
|
||||
if (previewEl) {
|
||||
previewEl.innerHTML = '';
|
||||
if (Array.isArray(item.items) && item.items.length > 0) {
|
||||
item.items.forEach((previewItem, idx) => {
|
||||
if (!previewItem || !previewItem.url) {
|
||||
console.warn('[GlobalSearch] Preview item missing url at index', idx, 'for key:', item.key, previewItem);
|
||||
return;
|
||||
}
|
||||
const previewLi = document.createElement('li');
|
||||
const previewLink = document.createElement('a');
|
||||
previewLink.href = previewItem.url;
|
||||
previewLink.textContent = previewItem.label || '';
|
||||
previewLi.appendChild(previewLink);
|
||||
previewEl.appendChild(previewLi);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (item.count > 0) {
|
||||
li.hidden = false;
|
||||
} else {
|
||||
li.hidden = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
setLoadingState('0');
|
||||
resultsEl.querySelectorAll('a').forEach((link) => {
|
||||
link.setAttribute('aria-disabled', 'true');
|
||||
const base = link.closest('[data-search-base]')?.getAttribute('data-search-base');
|
||||
link.setAttribute('href', base || '#');
|
||||
});
|
||||
resultsEl.querySelectorAll('[data-search-preview]').forEach((list) => {
|
||||
list.innerHTML = '';
|
||||
});
|
||||
resultsEl.querySelectorAll('[data-search-key]').forEach((li) => {
|
||||
li.hidden = false;
|
||||
});
|
||||
};
|
||||
|
||||
const fetchCounts = async (value) => {
|
||||
const url = new URL(endpoint.toString());
|
||||
url.searchParams.set('q', value);
|
||||
setLoadingState('…');
|
||||
const response = await fetch(url.toString(), { headers: { 'Accept': 'application/json' } });
|
||||
if (!response.ok) {
|
||||
console.warn('[GlobalSearch] Fetch failed with status:', response.status, response.statusText);
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
if (Array.isArray(data)) {
|
||||
render(data, value);
|
||||
} else {
|
||||
console.warn('[GlobalSearch] Invalid response format, expected array but got:', typeof data, data);
|
||||
clear();
|
||||
}
|
||||
};
|
||||
|
||||
const onInput = () => {
|
||||
const value = input.value.trim();
|
||||
if (value.length < minLength) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
if (pending) window.clearTimeout(pending);
|
||||
pending = window.setTimeout(() => {
|
||||
fetchCounts(value).catch((err) => {
|
||||
console.warn('[GlobalSearch] Fetch error:', err);
|
||||
clear();
|
||||
});
|
||||
}, 300);
|
||||
};
|
||||
|
||||
input.addEventListener('input', onInput);
|
||||
const submitSearch = () => {
|
||||
const value = input.value.trim();
|
||||
if (value.length < minLength) return;
|
||||
resultsPage.searchParams.set('search', value);
|
||||
window.location.href = resultsPage.toString();
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Enter') return;
|
||||
event.preventDefault();
|
||||
submitSearch();
|
||||
});
|
||||
|
||||
const form = input.closest('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
submitSearch();
|
||||
});
|
||||
}
|
||||
|
||||
if (toggleButton && panel) {
|
||||
toggleButton.addEventListener('click', () => {
|
||||
panel.classList.toggle('search-details-hidden');
|
||||
if (toggleIcon) {
|
||||
toggleIcon.classList.toggle('bi-dash-square');
|
||||
toggleIcon.classList.toggle('bi-plus-square');
|
||||
}
|
||||
});
|
||||
} else if (toggleButton || panel) {
|
||||
if (!toggleButton) console.warn('[GlobalSearch] [data-search-details-toggle] button not found');
|
||||
if (!panel) console.warn('[GlobalSearch] #aside-panel-search panel not found');
|
||||
}
|
||||
|
||||
// Seed from current URL search param if present
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const preset = currentUrl.searchParams.get('search');
|
||||
if (preset && !input.value) {
|
||||
input.value = preset;
|
||||
onInput();
|
||||
}
|
||||
}
|
||||
109
web/js/components/app-multiselect-init.js
Normal file
@@ -0,0 +1,109 @@
|
||||
let multiSelectLoader = null;
|
||||
|
||||
const resolveScriptUrl = (script) => {
|
||||
if (script) return script;
|
||||
const assetBase = window.APP_ASSET_BASE || document.baseURI;
|
||||
return new URL('vendor/multi-select/MultiSelect.js', assetBase).toString();
|
||||
};
|
||||
|
||||
const ensureMultiSelect = async (script) => {
|
||||
if (typeof window.MultiSelect !== 'undefined') {
|
||||
return window.MultiSelect;
|
||||
}
|
||||
if (!multiSelectLoader) {
|
||||
multiSelectLoader = new Promise((resolve, reject) => {
|
||||
const tag = document.createElement('script');
|
||||
tag.src = resolveScriptUrl(script);
|
||||
tag.async = true;
|
||||
tag.onload = () => resolve(window.MultiSelect);
|
||||
tag.onerror = (event) => reject(event);
|
||||
document.head.appendChild(tag);
|
||||
});
|
||||
}
|
||||
return multiSelectLoader;
|
||||
};
|
||||
|
||||
export const initMultiSelect = async (target = '[data-multi-select]') => {
|
||||
const elements = typeof target === 'string' ? document.querySelectorAll(target) : [target];
|
||||
if (!elements.length) return [];
|
||||
|
||||
const MultiSelect = await ensureMultiSelect();
|
||||
const instances = [];
|
||||
elements.forEach((element) => {
|
||||
if (!element || element.dataset.multiSelectInit === '1') return;
|
||||
const syncTarget = element.dataset.syncTarget;
|
||||
const syncInput = syncTarget ? document.querySelector(syncTarget) : null;
|
||||
const options = {};
|
||||
const placeholder = element.dataset.placeholder;
|
||||
if (placeholder) options.placeholder = placeholder;
|
||||
if (element.dataset.search !== undefined) {
|
||||
options.search = element.dataset.search === 'true';
|
||||
}
|
||||
if (element.dataset.selectAll !== undefined) {
|
||||
options.selectAll = element.dataset.selectAll === 'true';
|
||||
}
|
||||
if (element.dataset.disabled !== undefined) {
|
||||
options.disabled = element.dataset.disabled === 'true';
|
||||
}
|
||||
if (element.disabled) {
|
||||
options.disabled = true;
|
||||
}
|
||||
if (element.dataset.listAll !== undefined) {
|
||||
options.listAll = element.dataset.listAll === 'true';
|
||||
}
|
||||
if (element.dataset.selectedLabel) {
|
||||
options.selectedLabel = element.dataset.selectedLabel;
|
||||
}
|
||||
if (element.dataset.searchPlaceholder) {
|
||||
options.searchPlaceholder = element.dataset.searchPlaceholder;
|
||||
}
|
||||
if (element.dataset.selectAllLabel) {
|
||||
options.selectAllLabel = element.dataset.selectAllLabel;
|
||||
}
|
||||
let isSyncing = false;
|
||||
let instance;
|
||||
try {
|
||||
instance = new MultiSelect(element, options);
|
||||
} catch (error) {
|
||||
console.error('[multi-select] init failed', element, error);
|
||||
return;
|
||||
}
|
||||
if (element.dataset.disabled === 'true' || element.disabled) {
|
||||
instance.disable();
|
||||
}
|
||||
element.dataset.multiSelectInit = '1';
|
||||
instances.push(instance);
|
||||
if (syncInput) {
|
||||
syncInput._multiSelectInstance = instance;
|
||||
const syncValues = () => {
|
||||
if (!syncInput || isSyncing) return;
|
||||
isSyncing = true;
|
||||
const values = instance.selectedValues || [];
|
||||
syncInput.value = Array.isArray(values) ? values.join(',') : '';
|
||||
syncInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
isSyncing = false;
|
||||
};
|
||||
instance.element.addEventListener('multi-select:change', syncValues);
|
||||
const values = instance.selectedValues || [];
|
||||
syncInput.value = Array.isArray(values) ? values.join(',') : '';
|
||||
syncInput.addEventListener('change', () => {
|
||||
if (isSyncing) return;
|
||||
const nextValues = syncInput.value
|
||||
? syncInput.value.split(',').map((item) => item.trim()).filter(Boolean)
|
||||
: [];
|
||||
instance.setValues(nextValues);
|
||||
});
|
||||
}
|
||||
});
|
||||
return instances;
|
||||
};
|
||||
|
||||
const autoInit = () => {
|
||||
initMultiSelect().catch(() => {});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', autoInit);
|
||||
} else {
|
||||
autoInit();
|
||||
}
|
||||
48
web/js/components/app-nav-history.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const updateHistoryButtons = () => {
|
||||
const back = document.querySelector('#global-back');
|
||||
const forward = document.querySelector('#global-forward');
|
||||
if (!back && !forward) return;
|
||||
|
||||
const hasHistory = window.history.length > 1;
|
||||
|
||||
if (back) {
|
||||
back.setAttribute('aria-disabled', hasHistory ? 'false' : 'true');
|
||||
back.classList.toggle('is-disabled', !hasHistory);
|
||||
}
|
||||
if (forward) {
|
||||
forward.setAttribute('aria-disabled', hasHistory ? 'false' : 'true');
|
||||
forward.classList.toggle('is-disabled', !hasHistory);
|
||||
}
|
||||
};
|
||||
|
||||
const initHistoryButtons = () => {
|
||||
const back = document.querySelector('#global-back');
|
||||
const forward = document.querySelector('#global-forward');
|
||||
|
||||
if (back) {
|
||||
back.addEventListener('click', (event) => {
|
||||
if (window.history.length > 1) {
|
||||
event.preventDefault();
|
||||
window.history.back();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (forward) {
|
||||
forward.addEventListener('click', (event) => {
|
||||
if (window.history.length > 1) {
|
||||
event.preventDefault();
|
||||
window.history.forward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateHistoryButtons();
|
||||
window.addEventListener('popstate', updateHistoryButtons);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initHistoryButtons);
|
||||
} else {
|
||||
initHistoryButtons();
|
||||
}
|
||||
185
web/js/components/app-page-editor.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import EditorJS from '../../vendor/editorjs/editorjs.mjs';
|
||||
|
||||
const form = document.querySelector('[data-page-editor]');
|
||||
if (form) {
|
||||
const holder = form.querySelector('[data-editor-holder]');
|
||||
if (!holder) {
|
||||
// No holder available, skip initialization.
|
||||
} else {
|
||||
const contentField = form.querySelector('#page-content');
|
||||
const toggleButton = document.querySelector('[data-editor-toggle]');
|
||||
const saveButton = document.querySelector('[data-editor-save]');
|
||||
const canEdit = form.dataset.canEdit === '1';
|
||||
|
||||
const parseData = (value) => {
|
||||
if (!value) {
|
||||
return { blocks: [] };
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
if (!Array.isArray(parsed.blocks)) {
|
||||
parsed.blocks = [];
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore invalid JSON
|
||||
}
|
||||
return { blocks: [] };
|
||||
};
|
||||
|
||||
const setToggleLabel = (mode) => {
|
||||
if (!toggleButton) {
|
||||
return;
|
||||
}
|
||||
const viewLabel = toggleButton.dataset.viewLabel || '';
|
||||
const editLabel = toggleButton.dataset.editLabel || '';
|
||||
const text = mode === 'edit' ? viewLabel : editLabel;
|
||||
const icon = toggleButton.querySelector('[data-editor-icon]');
|
||||
|
||||
if (icon) {
|
||||
icon.className = mode === 'edit' ? 'bi bi-eye-fill' : 'bi bi-pencil-square';
|
||||
}
|
||||
toggleButton.setAttribute('aria-label', text);
|
||||
toggleButton.dataset.tooltip = text;
|
||||
};
|
||||
|
||||
let mode = form.dataset.editMode === 'edit' && canEdit ? 'edit' : 'view';
|
||||
form.dataset.editMode = mode;
|
||||
setToggleLabel(mode);
|
||||
|
||||
const HeaderTool = window.Header || window.EditorJSHeader || null;
|
||||
const ListTool = window.EditorjsList || window.List || null;
|
||||
const ChecklistTool = window.Checklist || window.EditorJSChecklist || null;
|
||||
const TableTool = window.Table || window.EditorJSTable || null;
|
||||
const ColumnsTool = window.editorjsColumns || window.Columns || null;
|
||||
const MarkerTool = window.Marker || null;
|
||||
|
||||
const tools = {};
|
||||
if (HeaderTool) {
|
||||
tools.header = {
|
||||
class: HeaderTool,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
placeholder: form.dataset.headerPlaceholder || 'Heading',
|
||||
levels: [1, 2, 3, 4],
|
||||
defaultLevel: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (ListTool) {
|
||||
tools.list = {
|
||||
class: ListTool,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
defaultStyle: 'unordered',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (ChecklistTool) {
|
||||
tools.checklist = {
|
||||
class: ChecklistTool,
|
||||
inlineToolbar: true,
|
||||
};
|
||||
}
|
||||
if (TableTool) {
|
||||
tools.table = {
|
||||
class: TableTool,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
rows: 2,
|
||||
cols: 2,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (ColumnsTool) {
|
||||
const innerTools = { ...tools };
|
||||
tools.columns = {
|
||||
class: ColumnsTool,
|
||||
config: {
|
||||
EditorJsLibrary: EditorJS,
|
||||
tools: innerTools,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (MarkerTool) {
|
||||
tools.marker = {
|
||||
class: MarkerTool,
|
||||
shortcut: 'CMD+SHIFT+M',
|
||||
};
|
||||
}
|
||||
|
||||
const inlineTools = ['bold', 'italic', 'link'];
|
||||
if (MarkerTool) {
|
||||
inlineTools.unshift('marker');
|
||||
}
|
||||
|
||||
const editor = new EditorJS({
|
||||
holder,
|
||||
data: parseData(contentField ? contentField.value : ''),
|
||||
readOnly: mode !== 'edit',
|
||||
autofocus: mode === 'edit',
|
||||
placeholder: form.dataset.placeholder || undefined,
|
||||
tools,
|
||||
inlineToolbar: inlineTools,
|
||||
});
|
||||
|
||||
const toggleMode = async (nextMode) => {
|
||||
if (!canEdit || mode === nextMode) {
|
||||
return;
|
||||
}
|
||||
mode = nextMode;
|
||||
form.dataset.editMode = mode;
|
||||
setToggleLabel(mode);
|
||||
if (editor.readOnly && editor.readOnly.toggle) {
|
||||
await editor.readOnly.toggle(mode !== 'edit');
|
||||
}
|
||||
if (saveButton) {
|
||||
saveButton.disabled = mode !== 'edit';
|
||||
}
|
||||
};
|
||||
|
||||
if (toggleButton && canEdit) {
|
||||
toggleButton.addEventListener('click', () => {
|
||||
toggleMode(mode === 'edit' ? 'view' : 'edit');
|
||||
});
|
||||
}
|
||||
|
||||
if (form && contentField && canEdit) {
|
||||
form.addEventListener('submit', async (event) => {
|
||||
if (form.dataset.submitting === '1') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const output = await editor.save();
|
||||
contentField.value = JSON.stringify(output);
|
||||
form.dataset.submitting = '1';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch(form.action || window.location.pathname, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
return;
|
||||
}
|
||||
const data = await response.json().catch(() => null);
|
||||
if (data && data.redirect) {
|
||||
window.location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
76
web/js/components/app-password-hints.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const rules = {
|
||||
min: (value, min) => value.length >= min,
|
||||
upper: (value) => /[A-Z]/.test(value),
|
||||
lower: (value) => /[a-z]/.test(value),
|
||||
number: (value) => /\d/.test(value),
|
||||
symbol: (value) => /[^a-zA-Z0-9]/.test(value),
|
||||
email: (value, _min, email) => {
|
||||
if (!email) return true;
|
||||
return !value.toLowerCase().includes(email.toLowerCase());
|
||||
},
|
||||
match: (_value, _min, _email, confirm) => {
|
||||
if (!confirm) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export function initPasswordHints() {
|
||||
const containers = document.querySelectorAll('[data-password-hints]');
|
||||
if (!containers.length) return;
|
||||
|
||||
containers.forEach((container) => {
|
||||
const passwordSelector = container.dataset.passwordInput;
|
||||
const confirmSelector = container.dataset.confirmInput;
|
||||
const emailSelector = container.dataset.emailInput;
|
||||
const passwordInput = passwordSelector ? document.querySelector(passwordSelector) : null;
|
||||
const confirmInput = confirmSelector ? document.querySelector(confirmSelector) : null;
|
||||
const emailInput = emailSelector ? document.querySelector(emailSelector) : null;
|
||||
const minLength = Number.parseInt(container.dataset.minLength || '12', 10);
|
||||
const items = container.querySelectorAll('[data-rule]');
|
||||
|
||||
const setState = (item, state) => {
|
||||
item.classList.remove('is-valid', 'is-invalid');
|
||||
if (state === 'valid') item.classList.add('is-valid');
|
||||
if (state === 'invalid') item.classList.add('is-invalid');
|
||||
};
|
||||
|
||||
const evaluate = () => {
|
||||
const password = passwordInput?.value ?? '';
|
||||
const confirm = confirmInput?.value ?? '';
|
||||
const email = emailInput?.value ?? '';
|
||||
const hasValue = password.length > 0 || confirm.length > 0;
|
||||
|
||||
items.forEach((item) => {
|
||||
const rule = item.dataset.rule;
|
||||
if (!rule) return;
|
||||
if (!hasValue) {
|
||||
setState(item, 'neutral');
|
||||
return;
|
||||
}
|
||||
if (rule === 'match') {
|
||||
if (password === '' || confirm === '') {
|
||||
setState(item, 'neutral');
|
||||
return;
|
||||
}
|
||||
setState(item, password === confirm ? 'valid' : 'invalid');
|
||||
return;
|
||||
}
|
||||
const check = rules[rule];
|
||||
if (!check) return;
|
||||
const ok = check(password, minLength, email, confirm);
|
||||
setState(item, ok ? 'valid' : 'invalid');
|
||||
});
|
||||
};
|
||||
|
||||
if (passwordInput) passwordInput.addEventListener('input', evaluate);
|
||||
if (confirmInput) confirmInput.addEventListener('input', evaluate);
|
||||
if (emailInput) emailInput.addEventListener('input', evaluate);
|
||||
evaluate();
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPasswordHints);
|
||||
} else {
|
||||
initPasswordHints();
|
||||
}
|
||||
118
web/js/components/app-tabs.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Initializes tab navigation
|
||||
* Usage:
|
||||
* - Tab buttons: <button data-tab="system">System</button>
|
||||
* - Tab panels: <div data-tab-panel="system">...</div>
|
||||
* - Container: <div data-tabs>...</div>
|
||||
* - URL param: ?tab=system
|
||||
*/
|
||||
export function initTabs(root = document) {
|
||||
const containers = root.querySelectorAll('[data-tabs]');
|
||||
|
||||
containers.forEach((container) => {
|
||||
if (container.dataset.tabsBound === '1') return;
|
||||
container.dataset.tabsBound = '1';
|
||||
|
||||
const tabs = Array.from(container.querySelectorAll('[data-tab]'));
|
||||
const panels = Array.from(container.querySelectorAll('[data-tab-panel]'));
|
||||
|
||||
if (!tabs.length || !panels.length) return;
|
||||
|
||||
const storageKey = container.dataset.tabsStorageKey || (container.id ? `tabs:${container.id}` : null);
|
||||
const urlParamName = container.dataset.tabsParam || 'tab';
|
||||
|
||||
// Helper to get URL parameter
|
||||
const getUrlParam = (name) => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(name);
|
||||
};
|
||||
|
||||
// Helper to set URL parameter
|
||||
const setUrlParam = (name, value) => {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set(name, value);
|
||||
window.history.replaceState({}, '', url);
|
||||
};
|
||||
|
||||
// Get initial active tab from URL param, localStorage, or data-tab-default
|
||||
let activeTab = null;
|
||||
const urlTab = getUrlParam(urlParamName);
|
||||
if (urlTab && panels.some(p => p.dataset.tabPanel === urlTab)) {
|
||||
activeTab = urlTab;
|
||||
} else if (storageKey) {
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored && panels.some(p => p.dataset.tabPanel === stored)) {
|
||||
activeTab = stored;
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage not available
|
||||
}
|
||||
}
|
||||
if (!activeTab) {
|
||||
const defaultTab = container.querySelector('[data-tab-default]');
|
||||
activeTab = defaultTab?.dataset.tab || tabs[0]?.dataset.tab;
|
||||
}
|
||||
|
||||
const activateTab = (tabName) => {
|
||||
tabs.forEach(tab => {
|
||||
const isActive = tab.dataset.tab === tabName;
|
||||
tab.classList.toggle('is-active', isActive);
|
||||
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
});
|
||||
|
||||
panels.forEach(panel => {
|
||||
const isVisible = panel.dataset.tabPanel === tabName;
|
||||
panel.hidden = !isVisible;
|
||||
panel.setAttribute('aria-hidden', isVisible ? 'false' : 'true');
|
||||
});
|
||||
|
||||
// Update URL parameter
|
||||
setUrlParam(urlParamName, tabName);
|
||||
|
||||
// Save to localStorage
|
||||
if (storageKey) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, tabName);
|
||||
} catch (e) {
|
||||
// localStorage not available
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize tabs
|
||||
tabs.forEach(tab => {
|
||||
tab.setAttribute('role', 'tab');
|
||||
tab.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
activateTab(tab.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
panels.forEach(panel => {
|
||||
panel.setAttribute('role', 'tabpanel');
|
||||
});
|
||||
|
||||
container.setAttribute('role', 'tablist');
|
||||
|
||||
// Activate initial tab
|
||||
if (activeTab) {
|
||||
activateTab(activeTab);
|
||||
}
|
||||
|
||||
// Listen for popstate (back/forward navigation)
|
||||
window.addEventListener('popstate', () => {
|
||||
const newTab = getUrlParam(urlParamName);
|
||||
if (newTab && panels.some(p => p.dataset.tabPanel === newTab)) {
|
||||
activateTab(newTab);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-initialize on load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initTabs());
|
||||
} else {
|
||||
initTabs();
|
||||
}
|
||||
71
web/js/components/app-tenant-switcher.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const switchTenant = async (link) => {
|
||||
const tenantId = link.dataset.switchTenant;
|
||||
const csrfKey = link.dataset.csrfKey;
|
||||
const csrfToken = link.dataset.csrfToken;
|
||||
const errorMessage = link.dataset.errorMessage || 'Failed to switch tenant';
|
||||
|
||||
if (!tenantId || !csrfKey || !csrfToken) {
|
||||
console.error('Missing tenant ID or CSRF token');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable link during request
|
||||
link.style.pointerEvents = 'none';
|
||||
link.style.opacity = '0.5';
|
||||
|
||||
try {
|
||||
const body = new URLSearchParams({
|
||||
tenant_id: tenantId,
|
||||
[csrfKey]: csrfToken
|
||||
});
|
||||
|
||||
const response = await fetch('admin/users/switch-tenant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'fetch'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
console.error('Tenant switch failed:', data.error);
|
||||
alert(errorMessage);
|
||||
link.style.pointerEvents = '';
|
||||
link.style.opacity = '';
|
||||
}
|
||||
} else {
|
||||
console.error('Server error:', response.status);
|
||||
alert(errorMessage);
|
||||
link.style.pointerEvents = '';
|
||||
link.style.opacity = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
alert(errorMessage);
|
||||
link.style.pointerEvents = '';
|
||||
link.style.opacity = '';
|
||||
}
|
||||
};
|
||||
|
||||
const initTenantSwitcher = () => {
|
||||
const tenantLinks = document.querySelectorAll('[data-switch-tenant]');
|
||||
|
||||
tenantLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
switchTenant(link);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTenantSwitcher);
|
||||
} else {
|
||||
initTenantSwitcher();
|
||||
}
|
||||
57
web/js/components/app-theme-toggle.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const setTheme = (theme) => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = theme;
|
||||
};
|
||||
|
||||
const setIcon = (button, theme) => {
|
||||
const icon = button.querySelector('i');
|
||||
if (!icon) return;
|
||||
icon.classList.remove('bi-sun-fill', 'bi-moon-stars-fill');
|
||||
icon.classList.add(theme === 'dark' ? 'bi-moon-stars-fill' : 'bi-sun-fill');
|
||||
};
|
||||
|
||||
const updateTheme = async (button, theme) => {
|
||||
const url = button.dataset.themeUrl;
|
||||
const csrfKey = button.dataset.csrfKey;
|
||||
const csrfToken = button.dataset.csrfToken;
|
||||
if (!url || !csrfKey || !csrfToken) {
|
||||
return true;
|
||||
}
|
||||
const body = new URLSearchParams({ theme, [csrfKey]: csrfToken });
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'fetch'
|
||||
},
|
||||
body
|
||||
});
|
||||
return response.ok;
|
||||
};
|
||||
|
||||
const initThemeToggle = () => {
|
||||
const button = document.querySelector('[data-theme-toggle]');
|
||||
if (!button) return;
|
||||
const root = document.documentElement;
|
||||
const getCurrent = () => (root.dataset.theme === 'dark' ? 'dark' : 'light');
|
||||
setIcon(button, getCurrent());
|
||||
|
||||
button.addEventListener('click', async () => {
|
||||
const current = getCurrent();
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
setIcon(button, next);
|
||||
const ok = await updateTheme(button, next);
|
||||
if (!ok) {
|
||||
setTheme(current);
|
||||
setIcon(button, current);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initThemeToggle);
|
||||
} else {
|
||||
initThemeToggle();
|
||||
}
|
||||
191
web/js/components/app-toggle-aside-panels.js
Normal file
@@ -0,0 +1,191 @@
|
||||
export function initAsidePanels(options = {}) {
|
||||
const {
|
||||
barSelector = '.aside-icon-bar',
|
||||
panelSelector = '.app-sidebar-panel',
|
||||
titleSelector = '.app-sidebar-title',
|
||||
toolsSelector = '[data-aside-tools]',
|
||||
revealSidebarOnActivate = true,
|
||||
persistSidebarStateOnActivate = true
|
||||
} = options;
|
||||
|
||||
const bar = document.querySelector(barSelector);
|
||||
if (!bar) return;
|
||||
|
||||
const tabs = Array.from(bar.querySelectorAll('[data-aside-target]'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
const panels = Array.from(document.querySelectorAll(panelSelector));
|
||||
const titleEl = document.querySelector(titleSelector);
|
||||
const toolsHost = document.querySelector(toolsSelector);
|
||||
const sidebarController = window.AppSidebar || null;
|
||||
const defaultTitle = titleEl?.dataset.asideTitleDefault || titleEl?.textContent || '';
|
||||
const storageKey = bar.dataset.asideStorage || '';
|
||||
|
||||
const getPanelKey = (panel) => panel?.dataset?.asidePanel || '';
|
||||
const getTabKey = (tab) => tab?.dataset?.asideTarget || '';
|
||||
const getPanelTools = (panel) => panel?.querySelector('[data-aside-panel-tools]') || null;
|
||||
const getTabHref = (tab) => tab?.dataset?.asideHref || '';
|
||||
|
||||
const initPanelDetails = (panel) => {
|
||||
if (!panel || panel.dataset.detailsBound === '1') return;
|
||||
const storageKey = panel.dataset.asideDetailsStorage || '';
|
||||
if (!storageKey) return;
|
||||
const detailsEls = Array.from(panel.querySelectorAll('details[data-details-key]'));
|
||||
if (!detailsEls.length) return;
|
||||
|
||||
const readStored = () => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(storageKey);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const writeStored = (value) => {
|
||||
try {
|
||||
const list = Array.isArray(value) ? value : [];
|
||||
window.localStorage.setItem(storageKey, JSON.stringify(list));
|
||||
} catch (error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
};
|
||||
|
||||
const applyStored = () => {
|
||||
const stored = readStored();
|
||||
if (!stored.length) return;
|
||||
detailsEls.forEach((details) => {
|
||||
details.open = stored.includes(details.dataset.detailsKey || '');
|
||||
});
|
||||
};
|
||||
|
||||
detailsEls.forEach((details) => {
|
||||
details.addEventListener('toggle', () => {
|
||||
const openKeys = detailsEls
|
||||
.filter((item) => item.open)
|
||||
.map((item) => item.dataset.detailsKey || '')
|
||||
.filter(Boolean);
|
||||
writeStored(openKeys);
|
||||
});
|
||||
});
|
||||
|
||||
applyStored();
|
||||
panel.dataset.detailsBound = '1';
|
||||
};
|
||||
|
||||
const readStored = () => {
|
||||
if (!storageKey) return null;
|
||||
try {
|
||||
return window.localStorage.getItem(storageKey);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeStored = (key) => {
|
||||
if (!storageKey) return;
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, key);
|
||||
} catch (error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
};
|
||||
|
||||
const syncTools = (panel) => {
|
||||
if (!toolsHost) return;
|
||||
toolsHost.innerHTML = '';
|
||||
const tools = getPanelTools(panel);
|
||||
if (!tools) return;
|
||||
toolsHost.innerHTML = tools.innerHTML;
|
||||
};
|
||||
|
||||
const openSidebar = (persist) => {
|
||||
if (!revealSidebarOnActivate || !sidebarController) return;
|
||||
if (!sidebarController.isCollapsed()) return;
|
||||
sidebarController.open({ persist });
|
||||
};
|
||||
|
||||
const setActive = (key, { fromUser = false } = {}) => {
|
||||
if (!key) return;
|
||||
|
||||
let activePanel = null;
|
||||
panels.forEach((panel) => {
|
||||
const isActive = getPanelKey(panel) === key;
|
||||
if (isActive) activePanel = panel;
|
||||
panel.hidden = !isActive;
|
||||
panel.setAttribute('aria-hidden', isActive ? 'false' : 'true');
|
||||
});
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
const isActive = getTabKey(tab) === key;
|
||||
tab.classList.toggle('active', isActive);
|
||||
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
tab.setAttribute('tabindex', isActive ? '0' : '-1');
|
||||
});
|
||||
|
||||
if (titleEl) {
|
||||
const nextTitle = activePanel?.dataset?.asideTitle || defaultTitle;
|
||||
if (nextTitle) {
|
||||
titleEl.textContent = nextTitle;
|
||||
}
|
||||
}
|
||||
if (activePanel) {
|
||||
syncTools(activePanel);
|
||||
initPanelDetails(activePanel);
|
||||
}
|
||||
writeStored(key);
|
||||
if (fromUser) {
|
||||
openSidebar(persistSidebarStateOnActivate);
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialKey = () => {
|
||||
const stored = readStored();
|
||||
if (stored && panels.some((panel) => getPanelKey(panel) === stored)) {
|
||||
return stored;
|
||||
}
|
||||
const activeTab = tabs.find((tab) => tab.classList.contains('active'));
|
||||
if (activeTab) return getTabKey(activeTab);
|
||||
const visiblePanel = panels.find((panel) => !panel.hidden);
|
||||
if (visiblePanel) return getPanelKey(visiblePanel);
|
||||
return getTabKey(tabs[0]);
|
||||
};
|
||||
|
||||
setActive(getInitialKey(), { fromUser: false });
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const key = getTabKey(tab);
|
||||
const isActive = tab.classList.contains('active');
|
||||
const href = getTabHref(tab);
|
||||
if (href) {
|
||||
const base = document.baseURI || window.location.origin;
|
||||
const targetUrl = new URL(href, base);
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const isSame = targetUrl.pathname === currentUrl.pathname && targetUrl.search === currentUrl.search;
|
||||
if (!isSame) {
|
||||
setActive(key, { fromUser: true });
|
||||
window.location.href = targetUrl.toString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (sidebarController && sidebarController.isCollapsed()) {
|
||||
sidebarController.open({ persist: persistSidebarStateOnActivate });
|
||||
setActive(key, { fromUser: true });
|
||||
return;
|
||||
}
|
||||
if (isActive && sidebarController) {
|
||||
sidebarController.close({ persist: true });
|
||||
return;
|
||||
}
|
||||
setActive(key, { fromUser: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initAsidePanels());
|
||||
} else {
|
||||
initAsidePanels();
|
||||
}
|
||||
66
web/js/components/app-toggle-details-aside.js
Normal file
@@ -0,0 +1,66 @@
|
||||
export function initDetailsAsideToggle(options = {}) {
|
||||
const {
|
||||
buttonSelector = '#toggle-main-content-aside',
|
||||
asideSelector = '#app-details-aside-section',
|
||||
containerSelector = '.app-details-container',
|
||||
storageKey = 'app-details-aside-collapsed'
|
||||
} = options;
|
||||
|
||||
const button = document.querySelector(buttonSelector);
|
||||
const aside = document.querySelector(asideSelector);
|
||||
if (!button || !aside) return;
|
||||
|
||||
const container = aside.closest(containerSelector) || document.querySelector(containerSelector);
|
||||
if (!container) return;
|
||||
|
||||
if (!button.getAttribute('aria-controls')) {
|
||||
button.setAttribute('aria-controls', aside.id || 'app-details-aside-section');
|
||||
}
|
||||
|
||||
const readStored = () => {
|
||||
try {
|
||||
return window.localStorage.getItem(storageKey);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeStored = (collapsed) => {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, collapsed ? '1' : '0');
|
||||
} catch (error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
};
|
||||
|
||||
const applyState = (collapsed, persist = false) => {
|
||||
container.classList.toggle('is-aside-collapsed', collapsed);
|
||||
aside.hidden = collapsed;
|
||||
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
if (persist) {
|
||||
writeStored(collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
const getInitialState = () => {
|
||||
const stored = readStored();
|
||||
if (stored === '1' || stored === '0') {
|
||||
return stored === '1';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
applyState(getInitialState(), false);
|
||||
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const collapsed = !container.classList.contains('is-aside-collapsed');
|
||||
applyState(collapsed, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initDetailsAsideToggle());
|
||||
} else {
|
||||
initDetailsAsideToggle();
|
||||
}
|
||||
95
web/js/components/app-toggle-sidebar.js
Normal file
@@ -0,0 +1,95 @@
|
||||
export function initSidebarToggle(options = {}) {
|
||||
const {
|
||||
buttonSelector = '#toggle-aside',
|
||||
asideSelector = '#app-sidebar',
|
||||
containerSelector = '.app-container',
|
||||
storageKey = 'app-sidebar-collapsed'
|
||||
} = options;
|
||||
|
||||
const aside = document.querySelector(asideSelector);
|
||||
if (!aside) return null;
|
||||
|
||||
const buttons = Array.from(document.querySelectorAll(buttonSelector));
|
||||
const container = document.querySelector(containerSelector);
|
||||
const root = document.documentElement;
|
||||
|
||||
const readStored = () => {
|
||||
try {
|
||||
return window.localStorage.getItem(storageKey);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeStored = (collapsed) => {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, collapsed ? '1' : '0');
|
||||
} catch (error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
};
|
||||
|
||||
const applyState = (collapsed, { persist = false } = {}) => {
|
||||
aside.classList.toggle('is-collapsed', collapsed);
|
||||
if (container) {
|
||||
container.classList.toggle('is-sidebar-collapsed', collapsed);
|
||||
}
|
||||
if (root) {
|
||||
root.classList.toggle('sidebar-collapsed', collapsed);
|
||||
}
|
||||
buttons.forEach((button) => {
|
||||
if (!button.getAttribute('aria-controls')) {
|
||||
button.setAttribute('aria-controls', aside.id || 'app-sidebar');
|
||||
}
|
||||
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
});
|
||||
if (persist) {
|
||||
writeStored(collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
const isCollapsed = () => aside.classList.contains('is-collapsed');
|
||||
|
||||
const getInitialState = () => {
|
||||
const stored = readStored();
|
||||
if (stored === '1' || stored === '0') {
|
||||
return stored === '1';
|
||||
}
|
||||
return root?.classList.contains('sidebar-collapsed') ?? false;
|
||||
};
|
||||
|
||||
const controller = {
|
||||
isCollapsed,
|
||||
open: ({ persist = true } = {}) => applyState(false, { persist }),
|
||||
close: ({ persist = true } = {}) => applyState(true, { persist }),
|
||||
toggle: ({ persist = true } = {}) => applyState(!isCollapsed(), { persist }),
|
||||
apply: (collapsed, { persist = false } = {}) => applyState(!!collapsed, { persist })
|
||||
};
|
||||
|
||||
applyState(getInitialState(), { persist: false });
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
controller.toggle({ persist: true });
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('app:sidebar', (event) => {
|
||||
const detail = event.detail || {};
|
||||
const action = detail.action;
|
||||
const persist = detail.persist ?? false;
|
||||
if (action === 'open') controller.open({ persist });
|
||||
if (action === 'close') controller.close({ persist });
|
||||
if (action === 'toggle') controller.toggle({ persist });
|
||||
});
|
||||
|
||||
window.AppSidebar = controller;
|
||||
return controller;
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initSidebarToggle());
|
||||
} else {
|
||||
initSidebarToggle();
|
||||
}
|
||||
86
web/js/components/app-toggle-toolbar.js
Normal file
@@ -0,0 +1,86 @@
|
||||
export function initToolbarToggles(root = document) {
|
||||
const toggles = root.querySelectorAll('[data-toolbar-toggle]');
|
||||
toggles.forEach((toggle) => {
|
||||
if (toggle.dataset.toolbarBound === '1') {
|
||||
return;
|
||||
}
|
||||
toggle.dataset.toolbarBound = '1';
|
||||
if (!toggle.hasAttribute('type')) {
|
||||
toggle.setAttribute('type', 'button');
|
||||
}
|
||||
|
||||
const targetSelector = toggle.dataset.toolbarTarget || toggle.getAttribute('aria-controls');
|
||||
if (!targetSelector) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targets = Array.from(root.querySelectorAll(targetSelector)).filter(Boolean);
|
||||
if (!targets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelTarget = toggle.querySelector('[data-toolbar-label]');
|
||||
const showLabel = toggle.dataset.toolbarTextShow;
|
||||
const hideLabel = toggle.dataset.toolbarTextHide;
|
||||
const storageKey =
|
||||
toggle.dataset.toolbarStorageKey ||
|
||||
(targets.length === 1 && targets[0].id
|
||||
? `toolbar:${targets[0].id}`
|
||||
: `toolbar:${targetSelector}`);
|
||||
|
||||
if (!toggle.getAttribute('aria-controls') && targets.length === 1 && targets[0].id) {
|
||||
toggle.setAttribute('aria-controls', targets[0].id);
|
||||
}
|
||||
|
||||
targets.forEach((target) => {
|
||||
if (target.dataset.toolbarInitialized === '1') {
|
||||
return;
|
||||
}
|
||||
const initial = target.dataset.toolbarInitial;
|
||||
if (initial === 'hidden') {
|
||||
target.hidden = true;
|
||||
} else if (initial === 'shown') {
|
||||
target.hidden = false;
|
||||
}
|
||||
target.dataset.toolbarInitialized = '1';
|
||||
});
|
||||
|
||||
if (storageKey) {
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored === 'hidden' || stored === 'shown') {
|
||||
const shouldShow = stored === 'shown';
|
||||
targets.forEach((target) => {
|
||||
target.hidden = !shouldShow;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// localStorage not available
|
||||
}
|
||||
}
|
||||
|
||||
const getExpanded = () => targets.every((target) => !target.hidden);
|
||||
const setExpanded = (expanded) => {
|
||||
targets.forEach((target) => {
|
||||
target.hidden = !expanded;
|
||||
});
|
||||
toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
if (labelTarget && (showLabel || hideLabel)) {
|
||||
labelTarget.textContent = expanded ? (hideLabel || labelTarget.textContent) : (showLabel || labelTarget.textContent);
|
||||
}
|
||||
if (storageKey) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, expanded ? 'shown' : 'hidden');
|
||||
} catch (error) {
|
||||
// localStorage not available
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setExpanded(getExpanded());
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
setExpanded(!getExpanded());
|
||||
});
|
||||
});
|
||||
}
|
||||
604
web/js/core/app-grid-factory.js
Normal file
@@ -0,0 +1,604 @@
|
||||
export function createServerGrid(options) {
|
||||
const {
|
||||
container,
|
||||
dataUrl,
|
||||
appBase = '/',
|
||||
columns,
|
||||
mapData,
|
||||
total = (data) => data.total,
|
||||
sortColumns = [],
|
||||
paginationLimit = 10,
|
||||
language = {},
|
||||
search = null,
|
||||
filters = [],
|
||||
urlSync = true,
|
||||
gridjs,
|
||||
rowDblClick = null,
|
||||
actions = null,
|
||||
rowDataset = null,
|
||||
selection = null
|
||||
} = options || {};
|
||||
|
||||
const gridjsLib = gridjs || window.gridjs;
|
||||
const containerEl = typeof container === 'string' ? document.querySelector(container) : container;
|
||||
if (!containerEl || !gridjsLib) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const resolveUrl = (path) => new URL(path, appBase).toString();
|
||||
const addParam = (url, key, value) => {
|
||||
const nextUrl = new URL(url);
|
||||
nextUrl.searchParams.set(key, value);
|
||||
return nextUrl.toString();
|
||||
};
|
||||
|
||||
const getInput = (input) => {
|
||||
if (!input) return null;
|
||||
return typeof input === 'string' ? document.querySelector(input) : input;
|
||||
};
|
||||
|
||||
const getInputValue = (input) => {
|
||||
if (!input) return '';
|
||||
if (input.type === 'checkbox') {
|
||||
return input.checked ? (input.value || '1') : '';
|
||||
}
|
||||
if (input.tagName === 'SELECT' && input.multiple) {
|
||||
return Array.from(input.selectedOptions || []).map((option) => option.value).filter(Boolean);
|
||||
}
|
||||
return (input.value ?? '').toString();
|
||||
};
|
||||
|
||||
const setInputValue = (input, value) => {
|
||||
if (!input) return;
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value === true || value === 'true' || value === '1';
|
||||
} else if (input.tagName === 'SELECT' && input.multiple) {
|
||||
const values = Array.isArray(value)
|
||||
? value.map((item) => item.toString())
|
||||
: value
|
||||
? value.toString().split(',').map((item) => item.trim()).filter(Boolean)
|
||||
: [];
|
||||
Array.from(input.options || []).forEach((option) => {
|
||||
option.selected = values.includes(option.value);
|
||||
});
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
if (input._multiSelectInstance && typeof input._multiSelectInstance.setValues === 'function') {
|
||||
const values = Array.isArray(value)
|
||||
? value.map((item) => item.toString())
|
||||
: value
|
||||
? value.toString().split(',').map((item) => item.trim()).filter(Boolean)
|
||||
: [];
|
||||
input._multiSelectInstance.setValues(values);
|
||||
}
|
||||
};
|
||||
|
||||
let searchValue = '';
|
||||
let searchParam = 'search';
|
||||
let searchInput = null;
|
||||
let searchDebounce = 250;
|
||||
if (search) {
|
||||
searchInput = getInput(search.input);
|
||||
searchParam = search.param || searchParam;
|
||||
searchDebounce = search.debounce ?? searchDebounce;
|
||||
const initial = params.get(searchParam) ?? search.default ?? '';
|
||||
searchValue = initial || '';
|
||||
if (searchInput) {
|
||||
setInputValue(searchInput, initial);
|
||||
}
|
||||
}
|
||||
|
||||
const filterState = filters.map((filter) => {
|
||||
const input = getInput(filter.input);
|
||||
const initial = params.get(filter.param);
|
||||
const value = initial !== null ? initial : (filter.default ?? '');
|
||||
if (input) {
|
||||
const parsed = typeof filter.parse === 'function' ? filter.parse(value) : value;
|
||||
setInputValue(input, parsed);
|
||||
}
|
||||
return { ...filter, input, value };
|
||||
});
|
||||
|
||||
const normalizeValue = (filter, value) => {
|
||||
if (typeof filter.normalize === 'function') {
|
||||
return filter.normalize(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const buildParams = () => {
|
||||
const query = new URLSearchParams();
|
||||
if (searchValue) {
|
||||
query.set(searchParam, searchValue);
|
||||
}
|
||||
filterState.forEach((filter) => {
|
||||
const raw = getInputValue(filter.input) || filter.value || '';
|
||||
const normalized = normalizeValue(filter, raw);
|
||||
if (Array.isArray(normalized)) {
|
||||
const list = normalized.map((item) => item.toString()).filter(Boolean);
|
||||
if (list.length) {
|
||||
query.set(filter.param, list.join(','));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (normalized !== '' && normalized !== null && typeof normalized !== 'undefined') {
|
||||
query.set(filter.param, normalized);
|
||||
}
|
||||
});
|
||||
return query;
|
||||
};
|
||||
|
||||
const baseUrl = () => {
|
||||
const url = new URL(dataUrl, appBase);
|
||||
const query = buildParams();
|
||||
query.forEach((value, key) => url.searchParams.set(key, value));
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const updateUrl = () => {
|
||||
if (!urlSync) return;
|
||||
const url = new URL(window.location.href);
|
||||
if (searchParam) {
|
||||
url.searchParams.delete(searchParam);
|
||||
}
|
||||
filterState.forEach((filter) => url.searchParams.delete(filter.param));
|
||||
const query = buildParams();
|
||||
query.forEach((value, key) => url.searchParams.set(key, value));
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
};
|
||||
|
||||
const actionConfig = actions && actions.enabled ? {
|
||||
label: actions.label || 'Actions',
|
||||
index: Number.isInteger(actions.index) ? actions.index : null,
|
||||
items: Array.isArray(actions.items) ? actions.items : [],
|
||||
getRowId: typeof actions.getRowId === 'function' ? actions.getRowId : null,
|
||||
wrapperClass: actions.wrapperClass || 'grid-actions',
|
||||
onAction: typeof actions.onAction === 'function' ? actions.onAction : null
|
||||
} : null;
|
||||
const selectionConfig = selection && selection.enabled ? {
|
||||
id: selection.id || 'selectRow',
|
||||
index: Number.isInteger(selection.index) ? selection.index : 0,
|
||||
component: selection.component || null,
|
||||
props: selection.props || null,
|
||||
onChange: typeof selection.onChange === 'function' ? selection.onChange : null,
|
||||
getSelectedIds: typeof selection.getSelectedIds === 'function' ? selection.getSelectedIds : null,
|
||||
selectAllLabel: selection.selectAllLabel || 'Select all',
|
||||
selectAllEnabled: selection.selectAllEnabled !== false
|
||||
} : null;
|
||||
|
||||
const insertActionColumn = (cols) => {
|
||||
if (!actionConfig) return cols;
|
||||
const actionColumn = {
|
||||
name: actionConfig.label,
|
||||
sort: false,
|
||||
formatter: (_cell, row) => {
|
||||
const id = actionConfig.getRowId ? actionConfig.getRowId(row) : null;
|
||||
if (!id) {
|
||||
return gridjsLib.html('');
|
||||
}
|
||||
const buttons = actionConfig.items.map((item) => {
|
||||
if (!item || !item.key) return '';
|
||||
const label = typeof item.label === 'function' ? item.label(row, id) : item.label;
|
||||
if (!label) return '';
|
||||
const className = item.className ? ` ${item.className}` : '';
|
||||
const type = item.type || 'button';
|
||||
if (type === 'link') {
|
||||
const href = typeof item.href === 'function' ? item.href({ row, id }) : item.href;
|
||||
const url = href || '#';
|
||||
return `<a role="button" class="${className.trim()}" data-grid-action="${item.key}" href="${url}">${label}</a>`;
|
||||
}
|
||||
return `<button type="button" class="${className.trim()}" data-grid-action="${item.key}">${label}</button>`;
|
||||
}).join('');
|
||||
return gridjsLib.html(`<div class="${actionConfig.wrapperClass}" role="group">${buttons}</div>`);
|
||||
}
|
||||
};
|
||||
|
||||
const next = [...cols];
|
||||
const index = actionConfig.index === null ? next.length : Math.min(Math.max(actionConfig.index, 0), next.length);
|
||||
next.splice(index, 0, actionColumn);
|
||||
return next;
|
||||
};
|
||||
|
||||
const insertSelectionColumn = (cols) => {
|
||||
if (!selectionConfig) return cols;
|
||||
const headerLabel = selectionConfig.selectAllLabel || 'Select all';
|
||||
const header = selectionConfig.selectAllEnabled
|
||||
? gridjsLib.html(`<input type="checkbox" data-grid-select-all aria-label="${headerLabel}">`)
|
||||
: '';
|
||||
const selectionColumn = {
|
||||
id: selectionConfig.id,
|
||||
name: header,
|
||||
sort: false,
|
||||
plugin: {
|
||||
id: selectionConfig.id
|
||||
}
|
||||
};
|
||||
|
||||
const next = [...cols];
|
||||
const index = Math.min(Math.max(selectionConfig.index ?? 0, 0), next.length);
|
||||
next.splice(index, 0, selectionColumn);
|
||||
return next;
|
||||
};
|
||||
|
||||
const insertActionSort = (cols) => {
|
||||
if (!actionConfig || !cols.length) return cols;
|
||||
const next = [...cols];
|
||||
const index = actionConfig.index === null ? next.length : Math.min(Math.max(actionConfig.index, 0), next.length);
|
||||
next.splice(index, 0, null);
|
||||
return next;
|
||||
};
|
||||
|
||||
const insertSelectionSort = (cols) => {
|
||||
if (!selectionConfig || !cols.length) return cols;
|
||||
const next = [...cols];
|
||||
const index = Math.min(Math.max(selectionConfig.index ?? 0, 0), next.length);
|
||||
next.splice(index, 0, null);
|
||||
return next;
|
||||
};
|
||||
|
||||
const mapDataWithActions = (data) => {
|
||||
const rows = mapData ? mapData(data) : [];
|
||||
if (!actionConfig || !Array.isArray(rows)) {
|
||||
return rows;
|
||||
}
|
||||
const index = actionConfig.index === null ? null : Math.min(Math.max(actionConfig.index, 0), Infinity);
|
||||
return rows.map((row) => {
|
||||
if (!Array.isArray(row)) return row;
|
||||
const next = [...row];
|
||||
const insertAt = index === null ? next.length : Math.min(index, next.length);
|
||||
next.splice(insertAt, 0, null);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const serverConfig = {
|
||||
url: baseUrl(),
|
||||
then: mapDataWithActions,
|
||||
total
|
||||
};
|
||||
|
||||
let finalSortColumns = sortColumns;
|
||||
if (selectionConfig) {
|
||||
finalSortColumns = insertSelectionSort(finalSortColumns);
|
||||
}
|
||||
if (actionConfig) {
|
||||
finalSortColumns = insertActionSort(finalSortColumns);
|
||||
}
|
||||
let currentSort = null;
|
||||
const sortConfig = finalSortColumns.length ? {
|
||||
enabled: true,
|
||||
server: {
|
||||
url: (prev, columns) => {
|
||||
if (!columns.length) {
|
||||
currentSort = null;
|
||||
return prev;
|
||||
}
|
||||
const column = columns[0];
|
||||
const key = finalSortColumns[column.index];
|
||||
if (!key) {
|
||||
currentSort = null;
|
||||
return prev;
|
||||
}
|
||||
const direction = column.direction === 1 ? 'asc' : 'desc';
|
||||
currentSort = { order: key, dir: direction };
|
||||
return addParam(addParam(prev, 'order', key), 'dir', direction);
|
||||
}
|
||||
}
|
||||
} : { enabled: false };
|
||||
|
||||
const paginationConfig = {
|
||||
limit: paginationLimit,
|
||||
server: {
|
||||
url: (prev, page, limit) => {
|
||||
const offset = page * limit;
|
||||
return addParam(addParam(prev, 'limit', limit), 'offset', offset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const plugins = [];
|
||||
if (selectionConfig?.component) {
|
||||
plugins.push({
|
||||
id: selectionConfig.id,
|
||||
component: selectionConfig.component,
|
||||
props: selectionConfig.props || {}
|
||||
});
|
||||
}
|
||||
|
||||
const grid = new gridjsLib.Grid({
|
||||
columns: insertActionColumn(insertSelectionColumn(columns)),
|
||||
server: serverConfig,
|
||||
search: false,
|
||||
sort: sortConfig,
|
||||
pagination: paginationConfig,
|
||||
language,
|
||||
plugins
|
||||
});
|
||||
|
||||
grid.render(containerEl);
|
||||
setTimeout(() => {
|
||||
initSelectAll();
|
||||
}, 0);
|
||||
|
||||
// Prevent empty-state flash before data arrives.
|
||||
let hasLoadedOnce = false;
|
||||
const markLoadedOnce = () => {
|
||||
if (hasLoadedOnce) return;
|
||||
hasLoadedOnce = true;
|
||||
containerEl.classList.add('gridjs-has-loaded');
|
||||
};
|
||||
const setUpdating = () => containerEl.classList.add('gridjs-is-updating');
|
||||
const clearUpdating = () => containerEl.classList.remove('gridjs-is-updating');
|
||||
|
||||
const store = grid?.config?.store;
|
||||
if (store?.subscribe) {
|
||||
store.subscribe((state) => {
|
||||
if (!state) return;
|
||||
// Grid.js status: 0=Init, 1=Loading, 2=Loaded, 3=Rendered, 4=Error
|
||||
if (state.status === 1) {
|
||||
setUpdating();
|
||||
} else {
|
||||
clearUpdating();
|
||||
}
|
||||
if (state.status >= 2) {
|
||||
markLoadedOnce();
|
||||
}
|
||||
if (state.status >= 3) {
|
||||
initSelectAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const applyRowDataset = () => {
|
||||
if (typeof rowDataset !== 'function') return;
|
||||
const rowEls = containerEl.querySelectorAll('.gridjs-tbody .gridjs-tr');
|
||||
const dataRows = grid.config.store.getState().data?.rows || [];
|
||||
rowEls.forEach((rowEl, index) => {
|
||||
const rowData = dataRows[index] || null;
|
||||
if (!rowData) return;
|
||||
const attrs = rowDataset(rowData, index) || {};
|
||||
Object.keys(attrs).forEach((key) => {
|
||||
const value = attrs[key];
|
||||
const attr = `data-${key}`;
|
||||
if (value === null || typeof value === 'undefined' || value === '') {
|
||||
rowEl.removeAttribute(attr);
|
||||
} else {
|
||||
rowEl.setAttribute(attr, String(value));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initSelectAll = () => {
|
||||
if (!selectionConfig?.component || !selectionConfig.selectAllEnabled) return;
|
||||
const header = containerEl.querySelector(`th[data-column-id="${selectionConfig.id}"] input[data-grid-select-all]`);
|
||||
if (!header) return;
|
||||
if (header.dataset.bound === '1') {
|
||||
return;
|
||||
}
|
||||
header.dataset.bound = '1';
|
||||
|
||||
const getRowCheckboxes = () => Array.from(
|
||||
containerEl.querySelectorAll(`td[data-column-id="${selectionConfig.id}"] input[type="checkbox"]`)
|
||||
);
|
||||
|
||||
const updateHeaderState = () => {
|
||||
const boxes = getRowCheckboxes();
|
||||
const total = boxes.length;
|
||||
const checked = boxes.filter((box) => box.checked).length;
|
||||
header.indeterminate = total > 0 && checked > 0 && checked < total;
|
||||
header.checked = total > 0 && checked === total;
|
||||
header.disabled = total === 0;
|
||||
};
|
||||
|
||||
header.addEventListener('change', () => {
|
||||
const boxes = getRowCheckboxes();
|
||||
boxes.forEach((box) => {
|
||||
if (box.checked !== header.checked) {
|
||||
box.click();
|
||||
}
|
||||
});
|
||||
updateHeaderState();
|
||||
});
|
||||
|
||||
updateHeaderState();
|
||||
};
|
||||
|
||||
if (typeof rowDataset === 'function') {
|
||||
const observer = new MutationObserver(() => applyRowDataset());
|
||||
observer.observe(containerEl, { childList: true, subtree: true });
|
||||
applyRowDataset();
|
||||
}
|
||||
|
||||
const getRowDataByElement = (rowEl) => {
|
||||
if (!rowEl) return null;
|
||||
const rows = Array.from(rowEl.parentElement?.children || []);
|
||||
const rowIndex = rows.indexOf(rowEl);
|
||||
if (rowIndex < 0) return null;
|
||||
const dataRows = grid.config.store.getState().data?.rows || [];
|
||||
const rowData = dataRows[rowIndex] || null;
|
||||
if (!rowData) return null;
|
||||
return { rowData, rowIndex };
|
||||
};
|
||||
|
||||
if (rowDblClick) {
|
||||
const excludeSelector = rowDblClick.exclude || '.grid-actions, button, a, input, select, textarea';
|
||||
containerEl.addEventListener('dblclick', (event) => {
|
||||
if (excludeSelector && event.target.closest(excludeSelector)) {
|
||||
return;
|
||||
}
|
||||
const rowEl = event.target.closest('.gridjs-tr');
|
||||
const rowInfo = getRowDataByElement(rowEl);
|
||||
if (!rowInfo) {
|
||||
return;
|
||||
}
|
||||
const { rowData, rowIndex } = rowInfo;
|
||||
const getUrl = rowDblClick.getUrl;
|
||||
if (typeof getUrl !== 'function') {
|
||||
return;
|
||||
}
|
||||
const url = getUrl(rowData, rowIndex);
|
||||
if (url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (actionConfig) {
|
||||
containerEl.addEventListener('click', async (event) => {
|
||||
const actionEl = event.target.closest('[data-grid-action]');
|
||||
if (!actionEl) {
|
||||
return;
|
||||
}
|
||||
const rowEl = actionEl.closest('.gridjs-tr');
|
||||
const rowInfo = getRowDataByElement(rowEl);
|
||||
if (!rowInfo) {
|
||||
return;
|
||||
}
|
||||
const { rowData } = rowInfo;
|
||||
const actionKey = actionEl.getAttribute('data-grid-action') || '';
|
||||
const actionItem = actionConfig.items.find((item) => item && item.key === actionKey);
|
||||
if (!actionItem) {
|
||||
return;
|
||||
}
|
||||
const id = actionConfig.getRowId ? actionConfig.getRowId(rowData) : null;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const confirmText = typeof actionItem.confirm === 'function'
|
||||
? actionItem.confirm({ row: rowData, id })
|
||||
: actionItem.confirm;
|
||||
if (confirmText && !window.confirm(confirmText)) {
|
||||
return;
|
||||
}
|
||||
if (actionItem.type === 'link') {
|
||||
const href = typeof actionItem.href === 'function'
|
||||
? actionItem.href({ row: rowData, id })
|
||||
: actionItem.href;
|
||||
if (href) {
|
||||
event.preventDefault();
|
||||
window.location.href = href;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (actionConfig.onAction) {
|
||||
event.preventDefault();
|
||||
await actionConfig.onAction({ action: actionKey, id, row: rowData, event, grid, container: containerEl });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const updateGrid = () => {
|
||||
updateUrl();
|
||||
grid.updateConfig({
|
||||
server: { ...serverConfig, url: baseUrl() }
|
||||
}).forceRender();
|
||||
};
|
||||
|
||||
const debounce = (fn, delay = 250) => {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', debounce((event) => {
|
||||
searchValue = event.target.value.trim();
|
||||
updateGrid();
|
||||
}, searchDebounce));
|
||||
}
|
||||
|
||||
filterState.forEach((filter) => {
|
||||
if (!filter.input) return;
|
||||
const eventName = filter.event || 'change';
|
||||
filter.input.addEventListener(eventName, () => {
|
||||
filter.value = getInputValue(filter.input);
|
||||
updateGrid();
|
||||
});
|
||||
});
|
||||
|
||||
const resetFilters = () => {
|
||||
if (search) {
|
||||
const defaultValue = search.default ?? '';
|
||||
searchValue = defaultValue;
|
||||
if (searchInput) {
|
||||
setInputValue(searchInput, defaultValue);
|
||||
}
|
||||
}
|
||||
filterState.forEach((filter) => {
|
||||
const defaultValue = filter.default ?? '';
|
||||
filter.value = defaultValue;
|
||||
if (filter.input) {
|
||||
setInputValue(filter.input, defaultValue);
|
||||
}
|
||||
});
|
||||
updateGrid();
|
||||
};
|
||||
|
||||
const getSortParams = () => (currentSort ? { ...currentSort } : null);
|
||||
const selectionApi = selectionConfig && selectionConfig.component ? {
|
||||
getSelectedIds: () => {
|
||||
if (selectionConfig.getSelectedIds) {
|
||||
return selectionConfig.getSelectedIds({ grid, container: containerEl });
|
||||
}
|
||||
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
|
||||
const store = plugin?.props?.store;
|
||||
const state = store?.state ?? store?.getState?.();
|
||||
if (!state) return [];
|
||||
if (Array.isArray(state.selected)) return state.selected;
|
||||
if (Array.isArray(state.ids)) return state.ids;
|
||||
if (state.rows && typeof state.rows === 'object') {
|
||||
return Object.keys(state.rows).filter((key) => state.rows[key]);
|
||||
}
|
||||
if (state instanceof Set) return Array.from(state);
|
||||
if (Array.isArray(state)) return state;
|
||||
if (typeof state === 'object') {
|
||||
return Object.keys(state).filter((key) => state[key]);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
clear: () => {
|
||||
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
|
||||
const store = plugin?.props?.store;
|
||||
if (store?.dispatch) {
|
||||
store.dispatch('reset');
|
||||
} else if (store?.reset) {
|
||||
store.reset();
|
||||
}
|
||||
}
|
||||
} : null;
|
||||
|
||||
if (selectionConfig?.onChange && selectionConfig.component) {
|
||||
grid.on('ready', () => {
|
||||
const plugin = grid?.config?.plugin?.get?.(selectionConfig.id);
|
||||
const store = plugin?.props?.store;
|
||||
const sync = () => {
|
||||
selectionConfig.onChange(selectionApi?.getSelectedIds?.() || []);
|
||||
};
|
||||
if (store?.on) store.on('updated', sync);
|
||||
if (store?.subscribe) store.subscribe(sync);
|
||||
sync();
|
||||
});
|
||||
}
|
||||
|
||||
grid.on('ready', () => {
|
||||
initSelectAll();
|
||||
setTimeout(() => {
|
||||
if (typeof window.requestFsLightboxRefresh === 'function') {
|
||||
window.requestFsLightboxRefresh();
|
||||
return;
|
||||
}
|
||||
if (typeof window.refreshFsLightbox === 'function') {
|
||||
window.refreshFsLightbox();
|
||||
} else {
|
||||
window.__fsLightboxRefreshPending = true;
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return { grid, container: containerEl, updateGrid, updateUrl, baseUrl, resolveUrl, getSortParams, resetFilters, selection: selectionApi };
|
||||
}
|
||||
19
web/js/pages/app-list-utils.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export const buildUrl = (appBase, path) => new URL(path, appBase).toString();
|
||||
|
||||
export const postAction = async (url, csrf, data = {}) => {
|
||||
const body = new URLSearchParams({ ...data, [csrf.key]: csrf.token });
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'fetch'
|
||||
},
|
||||
body
|
||||
});
|
||||
};
|
||||
|
||||
export const badgeHtml = (gridjs, value = '') => {
|
||||
const safe = value ?? '';
|
||||
return gridjs.html(`<span class="badge" data-variant="neutral">${safe}</span>`);
|
||||
};
|
||||
127
web/js/pages/app-users-list.js
Normal file
@@ -0,0 +1,127 @@
|
||||
export function initUsersListPage(options = {}) {
|
||||
const {
|
||||
gridConfig,
|
||||
appBase,
|
||||
csrf,
|
||||
labels = {},
|
||||
selectors = {},
|
||||
exportPath = 'admin/users/export',
|
||||
listPath = 'admin/users',
|
||||
buildBulkUrl,
|
||||
showFlash,
|
||||
withLoading
|
||||
} = options;
|
||||
|
||||
if (!gridConfig) return;
|
||||
|
||||
const exportSelector = selectors.exportButton || '[data-users-export]';
|
||||
const resetSelector = selectors.resetButton || '[data-users-reset]';
|
||||
const bulkSelector = selectors.bulkButtons || '[data-users-bulk]';
|
||||
|
||||
const resolveBulkUrl = buildBulkUrl
|
||||
? buildBulkUrl
|
||||
: (action) => new URL(`admin/users/bulk/${action}`, appBase).toString();
|
||||
|
||||
const postAction = async (url, data = {}) => {
|
||||
const body = new URLSearchParams({ ...data, [csrf.key]: csrf.token });
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'fetch'
|
||||
},
|
||||
body
|
||||
});
|
||||
};
|
||||
|
||||
const exportButton = document.querySelector(exportSelector);
|
||||
if (exportButton) {
|
||||
exportButton.addEventListener('click', () => {
|
||||
const exportUrl = new URL(exportPath, appBase);
|
||||
const dataUrl = new URL(gridConfig.baseUrl());
|
||||
dataUrl.searchParams.forEach((value, key) => exportUrl.searchParams.set(key, value));
|
||||
const sortParams = gridConfig.getSortParams ? gridConfig.getSortParams() : null;
|
||||
if (sortParams && sortParams.order) {
|
||||
exportUrl.searchParams.set('order', sortParams.order);
|
||||
exportUrl.searchParams.set('dir', sortParams.dir || 'asc');
|
||||
}
|
||||
window.location.href = exportUrl.toString();
|
||||
});
|
||||
}
|
||||
|
||||
const resetButton = document.querySelector(resetSelector);
|
||||
if (resetButton) {
|
||||
resetButton.addEventListener('click', () => {
|
||||
if (typeof gridConfig.resetFilters === 'function') {
|
||||
gridConfig.resetFilters();
|
||||
} else {
|
||||
gridConfig.updateGrid();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const bulkButtons = Array.from(document.querySelectorAll(bulkSelector));
|
||||
if (bulkButtons.length) {
|
||||
bulkButtons.forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
const action = button.getAttribute('data-users-bulk') || '';
|
||||
const ids = gridConfig.selection?.getSelectedIds?.() || [];
|
||||
const validActions = ['activate', 'deactivate', 'delete', 'send-access'];
|
||||
if (!ids.length || !validActions.includes(action)) {
|
||||
return;
|
||||
}
|
||||
const confirmTexts = {
|
||||
'activate': labels.bulkActivateConfirm,
|
||||
'deactivate': labels.bulkDeactivateConfirm,
|
||||
'delete': labels.bulkDeleteConfirm,
|
||||
'send-access': labels.bulkSendAccessConfirm
|
||||
};
|
||||
const confirmText = confirmTexts[action];
|
||||
if (confirmText && !window.confirm(confirmText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const executeBulk = async () => {
|
||||
const url = resolveBulkUrl(action);
|
||||
const response = await postAction(url, { uuids: ids.join(',') });
|
||||
const data = await response.json().catch(() => null);
|
||||
if (response.ok && data && data.ok) {
|
||||
gridConfig.selection?.clear?.();
|
||||
gridConfig.grid?.forceRender();
|
||||
|
||||
if (showFlash && labels.bulkMessages) {
|
||||
const msg = labels.bulkMessages[action];
|
||||
if (msg) {
|
||||
const count = data.count ?? 0;
|
||||
const failed = data.failed ?? 0;
|
||||
let text = msg.success.replace('%d', count);
|
||||
if (failed > 0 && msg.partial) {
|
||||
text = msg.partial.replace('%d', count).replace('%f', failed);
|
||||
}
|
||||
showFlash('success', text);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (showFlash && labels.bulkMessages) {
|
||||
const msg = labels.bulkMessages[action];
|
||||
if (msg?.error) {
|
||||
showFlash('error', msg.error);
|
||||
} else {
|
||||
window.location.href = new URL(listPath, appBase).toString();
|
||||
}
|
||||
} else {
|
||||
window.location.href = new URL(listPath, appBase).toString();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (withLoading) {
|
||||
await withLoading(executeBulk);
|
||||
} else {
|
||||
await executeBulk();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
4
web/vendor/bootstrap-icons/0-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.012 4.158c1.858 0 2.96-1.582 2.96-3.99V7.84c0-2.426-1.079-3.996-2.936-3.996-1.864 0-2.965 1.588-2.965 3.996v.328c0 2.42 1.09 3.99 2.941 3.99"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 476 B |
4
web/vendor/bootstrap-icons/0-circle.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
4
web/vendor/bootstrap-icons/0-square-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
4
web/vendor/bootstrap-icons/0-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-0-square" viewBox="0 0 16 16">
|
||||
<path d="M7.988 12.158c-1.851 0-2.941-1.57-2.941-3.99V7.84c0-2.408 1.101-3.996 2.965-3.996 1.857 0 2.935 1.57 2.935 3.996v.328c0 2.408-1.101 3.99-2.959 3.99M8 4.951c-1.008 0-1.629 1.09-1.629 2.895v.31c0 1.81.627 2.895 1.629 2.895s1.623-1.09 1.623-2.895v-.31c0-1.8-.621-2.895-1.623-2.895"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 579 B |
3
web/vendor/bootstrap-icons/1-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002H7.971L6.072 5.385v1.271l1.834-1.318h.065V12h1.312z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 250 B |
3
web/vendor/bootstrap-icons/1-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
3
web/vendor/bootstrap-icons/1-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm7.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
4
web/vendor/bootstrap-icons/1-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-1-square" viewBox="0 0 16 16">
|
||||
<path d="M9.283 4.002V12H7.971V5.338h-.065L6.072 6.656V5.385l1.899-1.383z"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
3
web/vendor/bootstrap-icons/123.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-123" viewBox="0 0 16 16">
|
||||
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 854 B |
3
web/vendor/bootstrap-icons/2-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24c0-.691.493-1.306 1.336-1.306.756 0 1.313.492 1.313 1.236 0 .697-.469 1.23-.902 1.705l-2.971 3.293V12h5.344v-1.107H7.268v-.077l1.974-2.22.096-.107c.688-.763 1.287-1.428 1.287-2.43 0-1.266-1.031-2.215-2.613-2.215-1.758 0-2.637 1.19-2.637 2.402v.065h1.271v-.07Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
3
web/vendor/bootstrap-icons/2-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8m15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 477 B |
3
web/vendor/bootstrap-icons/2-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm4.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
4
web/vendor/bootstrap-icons/2-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-2-square" viewBox="0 0 16 16">
|
||||
<path d="M6.646 6.24v.07H5.375v-.064c0-1.213.879-2.402 2.637-2.402 1.582 0 2.613.949 2.613 2.215 0 1.002-.6 1.667-1.287 2.43l-.096.107-1.974 2.22v.077h3.498V12H5.422v-.832l2.97-3.293c.434-.475.903-1.008.903-1.705 0-.744-.557-1.236-1.313-1.236-.843 0-1.336.615-1.336 1.306"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 564 B |
3
web/vendor/bootstrap-icons/3-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.082.414c.92 0 1.535.54 1.541 1.318.012.791-.615 1.36-1.588 1.354-.861-.006-1.482-.469-1.54-1.066H5.104c.047 1.177 1.05 2.144 2.754 2.144 1.653 0 2.954-.937 2.93-2.396-.023-1.278-1.031-1.846-1.734-1.916v-.07c.597-.1 1.505-.739 1.482-1.876-.03-1.177-1.043-2.074-2.637-2.062-1.675.006-2.59.984-2.625 2.12h1.248c.036-.556.557-1.054 1.348-1.054.785 0 1.348.486 1.348 1.195.006.715-.563 1.237-1.342 1.237h-.838v1.072h.879Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 607 B |
4
web/vendor/bootstrap-icons/3-circle.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 642 B |
3
web/vendor/bootstrap-icons/3-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 634 B |
4
web/vendor/bootstrap-icons/3-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-3-square" viewBox="0 0 16 16">
|
||||
<path d="M7.918 8.414h-.879V7.342h.838c.78 0 1.348-.522 1.342-1.237 0-.709-.563-1.195-1.348-1.195-.79 0-1.312.498-1.348 1.055H5.275c.036-1.137.95-2.115 2.625-2.121 1.594-.012 2.608.885 2.637 2.062.023 1.137-.885 1.776-1.482 1.875v.07c.703.07 1.71.64 1.734 1.917.024 1.459-1.277 2.396-2.93 2.396-1.705 0-2.707-.967-2.754-2.144H6.33c.059.597.68 1.06 1.541 1.066.973.006 1.6-.563 1.588-1.354-.006-.779-.621-1.318-1.541-1.318"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 714 B |
3
web/vendor/bootstrap-icons/4-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M7.519 5.057c-.886 1.418-1.772 2.838-2.542 4.265v1.12H8.85V12h1.26v-1.559h1.007V9.334H10.11V4.002H8.176zM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
4
web/vendor/bootstrap-icons/4-circle.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-circle" viewBox="0 0 16 16">
|
||||
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
4
web/vendor/bootstrap-icons/4-square-fill.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 428 B |
4
web/vendor/bootstrap-icons/4-square.svg
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-4-square" viewBox="0 0 16 16">
|
||||
<path d="M7.519 5.057q.33-.527.657-1.055h1.933v5.332h1.008v1.107H10.11V12H8.85v-1.559H4.978V9.322c.77-1.427 1.656-2.847 2.542-4.265ZM6.225 9.281v.053H8.85V5.063h-.065c-.867 1.33-1.787 2.806-2.56 4.218"/>
|
||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 493 B |
3
web/vendor/bootstrap-icons/5-circle-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle-fill" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-8.006 4.158c1.74 0 2.924-1.119 2.924-2.806 0-1.641-1.178-2.584-2.56-2.584-.897 0-1.442.421-1.612.68h-.064l.193-2.344h3.621V4.002H5.791L5.445 8.63h1.149c.193-.358.668-.809 1.435-.809.85 0 1.582.604 1.582 1.57 0 1.085-.779 1.682-1.57 1.682-.697 0-1.389-.31-1.53-1.031H5.276c.065 1.213 1.149 2.115 2.72 2.115Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 495 B |
3
web/vendor/bootstrap-icons/5-circle.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-circle" viewBox="0 0 16 16">
|
||||
<path d="M1 8a7 7 0 1 1 14 0A7 7 0 0 1 1 8m15 0A8 8 0 1 0 0 8a8 8 0 0 0 16 0m-8.006 4.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 514 B |
3
web/vendor/bootstrap-icons/5-square-fill.svg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-5-square-fill" viewBox="0 0 16 16">
|
||||
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm5.994 12.158c-1.57 0-2.654-.902-2.719-2.115h1.237c.14.72.832 1.031 1.529 1.031.791 0 1.57-.597 1.57-1.681 0-.967-.732-1.57-1.582-1.57-.767 0-1.242.45-1.435.808H5.445L5.791 4h4.705v1.103H6.875l-.193 2.343h.064c.17-.258.715-.68 1.611-.68 1.383 0 2.561.944 2.561 2.585 0 1.687-1.184 2.806-2.924 2.806Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 521 B |