This commit is contained in:
2026-02-04 23:31:53 +01:00
commit cd59ccd99b
2401 changed files with 56808 additions and 0 deletions

BIN
web/.DS_Store vendored Normal file

Binary file not shown.

9
web/.htaccess Normal file
View 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
View 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
View 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;
}

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,7 @@
.app-list-table table {
margin: 0;
}
.app-list-table {
white-space: nowrap;
}

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

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

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

View File

@@ -0,0 +1,10 @@
.app-page-copy .app-field {
min-width: 200px;
}
.app-page-copy .app-field [role="group"] {
margin-bottom: 0;
}

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

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

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

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

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

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

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

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

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

View 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

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -0,0 +1,10 @@
.back_to_login {
margin-bottom: 1rem;
}
@media (min-width: 600px) {
main {
min-height: 90vh;
place-content: center;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
web/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

36
web/favicon/favicon.svg Normal file
View 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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
web/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
web/img/minty_square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
web/img/mintyphp_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

159
web/index.php Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
import './components/app-flash-auto-dismiss.js';
import './components/app-password-hints.js';

View 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 = '';
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Some files were not shown because too many files have changed in this diff Show More