big restructure
This commit is contained in:
@@ -26,8 +26,8 @@
|
||||
--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-form-element-spacing-vertical: 0.5rem;
|
||||
--app-form-element-spacing-horizontal: 0.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(
|
||||
@@ -53,6 +53,21 @@
|
||||
--app-secondary-h-base: 223;
|
||||
--app-secondary-s-base: 19%;
|
||||
--app-secondary-l-base: 45%;
|
||||
--app-blockquote-background-color: var(
|
||||
--app-form-element-active-background-color
|
||||
);
|
||||
--list-tabs-border: var(--app-border);
|
||||
--list-tabs-active: var(--app-primary);
|
||||
--list-tabs-muted: var(--app-muted-color);
|
||||
--tabs-border: var(--app-border);
|
||||
--tabs-active: var(--app-primary);
|
||||
--tabs-muted: var(--app-muted-color);
|
||||
--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;
|
||||
--app-badge-background-color: coral;
|
||||
}
|
||||
|
||||
@@ -104,8 +119,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
:host(:not([data-theme="dark"])),
|
||||
:root:not([data-theme="dark"]),
|
||||
:host(:not([data-theme])),
|
||||
:root:not([data-theme]),
|
||||
[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--app-background-color: #fff;
|
||||
@@ -208,6 +223,29 @@
|
||||
--app-mark-color: #0f1114;
|
||||
--app-ins-color: rgb(28.5, 105.5, 84);
|
||||
--app-del-color: rgb(136, 56.5, 53);
|
||||
--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;
|
||||
--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);
|
||||
--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);
|
||||
@@ -282,7 +320,7 @@
|
||||
--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"]))
|
||||
:host(:not([data-theme]))
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
@@ -291,7 +329,7 @@
|
||||
[type="radio"],
|
||||
[type="file"]
|
||||
),
|
||||
:root:not([data-theme="dark"])
|
||||
:root:not([data-theme])
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
@@ -426,6 +464,29 @@
|
||||
--app-mark-color: #fff;
|
||||
--app-ins-color: #62af9a;
|
||||
--app-del-color: rgb(205.5, 126, 123);
|
||||
--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;
|
||||
--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-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);
|
||||
@@ -510,7 +571,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
[data-theme^="dark"] {
|
||||
color-scheme: dark;
|
||||
--app-background-color: rgb(19, 22.5, 30.5);
|
||||
--app-sidebar-background-color: rgb(18 20 27);
|
||||
@@ -621,6 +682,29 @@
|
||||
--app-mark-color: #fff;
|
||||
--app-ins-color: #62af9a;
|
||||
--app-del-color: rgb(205.5, 126, 123);
|
||||
--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;
|
||||
--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-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);
|
||||
@@ -695,7 +779,7 @@
|
||||
--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"]
|
||||
[data-theme^="dark"]
|
||||
input:is(
|
||||
[type="submit"],
|
||||
[type="button"],
|
||||
44
web/css/base/variables.contrast.css
Normal file
44
web/css/base/variables.contrast.css
Normal file
@@ -0,0 +1,44 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
/* High contrast overrides */
|
||||
html[data-contrast="high"][data-theme="light"],
|
||||
[data-contrast="high"][data-theme="light"] {
|
||||
--app-background-color: #fff;
|
||||
--app-sidebar-background-color: #fff;
|
||||
--app-color: #0f172a;
|
||||
--app-muted-color: #1f2937;
|
||||
--app-muted-border-color: #64748b;
|
||||
--app-form-element-background-color: #fff;
|
||||
--app-form-element-border-color: #475569;
|
||||
--app-form-element-color: #0f172a;
|
||||
--app-form-element-placeholder-color: #334155;
|
||||
--app-table-border-color: var(--app-muted-border-color);
|
||||
--app-card-border-color: var(--app-muted-border-color);
|
||||
--app-dropdown-border-color: var(--app-muted-border-color);
|
||||
--app-accordion-border-color: white;
|
||||
--app-card-background-color: #ffffff;
|
||||
--app-blockquote-border-color: var(--app-muted-border-color);
|
||||
--app-tooltip-background-color: #000;
|
||||
--app-tooltip-color: #fff;
|
||||
}
|
||||
|
||||
html[data-contrast="high"][data-theme^="dark"],
|
||||
[data-contrast="high"][data-theme^="dark"] {
|
||||
--app-background-color: #000;
|
||||
--app-sidebar-background-color: #000;
|
||||
--app-color: #f8fafc;
|
||||
--app-muted-color: #cbd5e1;
|
||||
--app-muted-border-color: #64748b;
|
||||
--app-form-element-background-color: #000000;
|
||||
--app-form-element-border-color: #94a3b8;
|
||||
--app-form-element-color: #f8fafc;
|
||||
--app-form-element-placeholder-color: #cbd5e1;
|
||||
--app-table-border-color: var(--app-muted-border-color);
|
||||
--app-card-border-color: var(--app-muted-border-color);
|
||||
--app-dropdown-border-color: var(--app-muted-border-color);
|
||||
--app-accordion-border-color: black;
|
||||
--app-card-background-color: #000000;
|
||||
--app-blockquote-border-color: var(--app-muted-border-color);
|
||||
--app-tooltip-background-color: #fff;
|
||||
--app-tooltip-color: #0b0f14;
|
||||
}
|
||||
42
web/css/base/variables.theme-dark-green.css
Normal file
42
web/css/base/variables.theme-dark-green.css
Normal file
@@ -0,0 +1,42 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
[data-theme="dark-green"] {
|
||||
--app-background-color: #0f1713;
|
||||
--app-sidebar-background-color: #0d1511;
|
||||
--app-color: #cfe0d6;
|
||||
--app-text-selection-color: rgba(64, 170, 118, 0.25);
|
||||
--app-muted-color: #8a9b91;
|
||||
--app-muted-border-color: #223028;
|
||||
--app-form-element-background-color: #141f19;
|
||||
--app-form-element-border-color: #26362d;
|
||||
--app-form-element-color: #d5e6dc;
|
||||
--app-form-element-placeholder-color: #7f9187;
|
||||
--app-card-background-color: #141f19;
|
||||
--app-card-border-color: #223028;
|
||||
--app-card-sectioning-background-color: #1a261f;
|
||||
--app-dropdown-background-color: #141f19;
|
||||
--app-dropdown-border-color: #223028;
|
||||
--app-dropdown-hover-background-color: #1f2a24;
|
||||
--app-modal-overlay-background-color: rgba(7, 10, 8, 0.75);
|
||||
--app-progress-background-color: #223028;
|
||||
--app-primary-h-base: 145;
|
||||
--app-primary-s-base: 38%;
|
||||
--app-primary-l-base: 38%;
|
||||
--badge-success-bg: #123225;
|
||||
--badge-success-color: #8de2b4;
|
||||
--badge-success-border: #1f4a36;
|
||||
--badge-danger-bg: #361a1d;
|
||||
--badge-danger-color: #f4a3ad;
|
||||
--badge-danger-border: #5a2b32;
|
||||
--badge-info-bg: #142735;
|
||||
--badge-info-color: #9ad2e0;
|
||||
--badge-info-border: #214156;
|
||||
--badge-warn-bg: #3a2a12;
|
||||
--badge-warn-color: #ffdf9e;
|
||||
--badge-warn-border: #5a401b;
|
||||
--badge-neutral-bg: #1b2620;
|
||||
--badge-neutral-color: #cfe0d6;
|
||||
--badge-neutral-border: #2a3a31;
|
||||
--app-tile-bg: rgb(20 28 24);
|
||||
--app-blockquote-background-color: rgb(20 28 24);
|
||||
}
|
||||
@@ -1,39 +1,3 @@
|
||||
: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;
|
||||
@@ -121,6 +85,22 @@
|
||||
border-color: var(--badge-neutral-border);
|
||||
}
|
||||
|
||||
.badge .badge-color-dot {
|
||||
width: 0.85em;
|
||||
height: 0.85em;
|
||||
border-radius: 999px;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--app-border-color);
|
||||
background: currentColor;
|
||||
box-shadow: 0 0 0 2px var(--app-background-color) inset;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .badge .badge-color-dot {
|
||||
border-color: var(--app-border-color);
|
||||
box-shadow: 0 0 0 2px var(--app-background-color) inset;
|
||||
}
|
||||
|
||||
.badge-list {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
:root {
|
||||
--app-blockquote-background-color: var(
|
||||
--app-form-element-active-background-color
|
||||
);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
--blockquote-bg: var(--app-blockquote-background-color);
|
||||
--blockquote-color: inherit;
|
||||
|
||||
@@ -96,20 +96,31 @@
|
||||
}
|
||||
|
||||
.app-details-container > section > * {
|
||||
padding-inline: calc(var(--app-spacing) * 2);
|
||||
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);
|
||||
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);
|
||||
position: sticky;
|
||||
top: 69px;
|
||||
background: var(--app-background-color);
|
||||
}
|
||||
|
||||
.app-details-container:not(:has(.app-breadcrumb)) .app-details-titlebar {
|
||||
top: 37px;
|
||||
padding-block-start: var(--app-spacing);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-details-container > section > * {
|
||||
padding-inline: calc(var(--app-spacing) * 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 968px) {
|
||||
@@ -135,3 +146,5 @@
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
--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);
|
||||
/* 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);
|
||||
/* margin-bottom: var(--app-spacing); */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
.notice ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.notice li:last-child {
|
||||
@@ -114,6 +114,17 @@
|
||||
padding-block-start: 1rem;
|
||||
}
|
||||
|
||||
.flash-stack:has(.notice) {
|
||||
/* .flash-stack:has(.notice) {
|
||||
margin-top: var(--app-spacing);
|
||||
} */
|
||||
|
||||
.app-main-content .flash-stack {
|
||||
margin-bottom: var(--app-spacing);
|
||||
}
|
||||
.app-main-content:has(.app-details-container) .flash-stack {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.notice li {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
: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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.app-list-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: calc(var(--app-spacing) * 1);
|
||||
@@ -22,6 +22,14 @@
|
||||
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(
|
||||
|
||||
@@ -37,15 +37,17 @@ ul.app-search-preview {
|
||||
.search-details-hidden ul.app-search-preview {
|
||||
display: none;
|
||||
}
|
||||
ul.app-search-preview::before {
|
||||
ul.app-search-preview li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 9px;
|
||||
top: 2px;
|
||||
bottom: 20px;
|
||||
width: 1px;
|
||||
background: var(--app-border);
|
||||
border-radius: 999px;
|
||||
left: 17px;
|
||||
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 li {
|
||||
@@ -56,7 +58,7 @@ ul.app-search-preview li {
|
||||
ul.app-search-preview li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -11px;
|
||||
left: 17px;
|
||||
bottom: 11px;
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
@@ -67,7 +69,7 @@ ul.app-search-preview li::before {
|
||||
}
|
||||
|
||||
ul.app-search-preview a {
|
||||
padding-inline: 0;
|
||||
padding-inline: 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
:root {
|
||||
--tabs-border: var(--app-border);
|
||||
--tabs-active: var(--app-primary);
|
||||
--tabs-muted: var(--app-muted-color);
|
||||
}
|
||||
|
||||
.app-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -17,6 +11,7 @@
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-block-start: 2px;
|
||||
padding-left: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-tabs-nav button,
|
||||
@@ -26,7 +21,6 @@
|
||||
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;
|
||||
|
||||
@@ -1,51 +1,27 @@
|
||||
: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);
|
||||
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);
|
||||
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;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.app-stats-table table {
|
||||
margin: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-stats-table table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.app-tile {
|
||||
@@ -62,7 +38,10 @@
|
||||
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);
|
||||
transition:
|
||||
transform var(--app-transition),
|
||||
box-shadow var(--app-transition),
|
||||
border-color var(--app-transition);
|
||||
}
|
||||
|
||||
.app-tile:hover {
|
||||
@@ -82,18 +61,21 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
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);
|
||||
display: block;
|
||||
margin-top: calc(var(--app-spacing) * 1);
|
||||
color: var(--tile-label-color);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
max-width: 191px;
|
||||
}
|
||||
|
||||
.app-tile-link {
|
||||
@@ -104,6 +86,43 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.app-tile.is-small {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 50px;
|
||||
}
|
||||
.app-tile.is-small span.app-tile-icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.app-tile.is-small .app-tile-link {
|
||||
bottom: 14px;
|
||||
}
|
||||
|
||||
.app-tile.is-small .app-tile-count {
|
||||
position: static;
|
||||
margin-right: 5px;
|
||||
display: block;
|
||||
}
|
||||
.app-tile.is-small .app-tile-label {
|
||||
margin-top: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.app-stats-table-header {
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
padding: 4px 10px 4px 10px;
|
||||
text-transform: uppercase;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.4px;
|
||||
background: var(--app-blockquote-background-color);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app-tiles {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
: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;
|
||||
|
||||
@@ -90,7 +90,7 @@ input.gridjs-input:focus {
|
||||
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;
|
||||
padding: 1px 10px;
|
||||
}
|
||||
|
||||
.gridjs-pagination .gridjs-pages button:hover {
|
||||
@@ -186,6 +186,11 @@ html[data-theme=dark] button.gridjs-sort-neutral {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
html[data-theme=dark-green] button.gridjs-sort,
|
||||
html[data-theme=dark-green] button.gridjs-sort-neutral {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.gridjs-tbody, td.gridjs-td {
|
||||
background-color: var(--app-background-color)
|
||||
}
|
||||
|
||||
@@ -1,41 +1,4 @@
|
||||
|
||||
: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;
|
||||
@@ -128,3 +91,13 @@
|
||||
.multi-select .multi-select-header {
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
.multi-select .multi-select-option.is-disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.multi-select .multi-select-all.is-disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -1,60 +1,106 @@
|
||||
aside.aside-icon-bar {
|
||||
--app-icon-bar-item-padding: 0.5rem;
|
||||
--app-icon-bar-item-size: 20px;
|
||||
--app-icon-bar-border-width: 3px;
|
||||
--app-icon-bar-hover-bg: var(--app-accordion-border-color);
|
||||
--app-icon-bar-border: 1px solid var(--app-border);
|
||||
display: none;
|
||||
}
|
||||
|
||||
aside.aside-icon-bar ul {
|
||||
aside.aside-icon-bar nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
aside.aside-icon-bar ul,
|
||||
aside.aside-icon-bar li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
aside.aside-icon-bar ul {
|
||||
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;
|
||||
padding: var(--app-icon-bar-item-padding);
|
||||
font-size: var(--app-icon-bar-item-size);
|
||||
text-align: center;
|
||||
color: var(--app-muted-color);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-left: 3px solid transparent;
|
||||
border-left: var(--app-icon-bar-border-width) 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 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: var(--app-icon-bar-border-width) solid var(--app-primary);
|
||||
color: var(--app-contrast);
|
||||
background: var(--app-icon-bar-hover-bg);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
border-right: var(--app-icon-bar-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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
aside.aside-icon-bar {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
border-top: var(--app-icon-bar-border);
|
||||
background: var(--app-background-color);
|
||||
}
|
||||
|
||||
aside.aside-icon-bar nav {
|
||||
flex-direction: row;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
gap: var(--app-spacing);
|
||||
}
|
||||
|
||||
aside.aside-icon-bar ul {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: var(--app-spacing);
|
||||
}
|
||||
|
||||
aside.aside-icon-bar li a,
|
||||
aside.aside-icon-bar li button {
|
||||
border-left: 0;
|
||||
border-top: var(--app-icon-bar-border-width) solid transparent;
|
||||
}
|
||||
|
||||
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: 0;
|
||||
border-top: var(--app-icon-bar-border-width) solid var(--app-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,15 @@ small {
|
||||
}
|
||||
|
||||
div#debugger-bar {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-container.is-sidebar-hidden .app-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-hidden .app-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1,
|
||||
@@ -27,13 +35,13 @@ h6 {
|
||||
}
|
||||
|
||||
h1 {
|
||||
--app-font-size: 1.8rem;
|
||||
--app-font-size: 1.5rem;
|
||||
--app-line-height: 1.125;
|
||||
--app-typography-spacing-top: 3rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
--app-font-size: 1.6rem;
|
||||
--app-font-size: 1.4rem;
|
||||
--app-line-height: 1.15;
|
||||
--app-typography-spacing-top: 2.625rem;
|
||||
}
|
||||
@@ -179,7 +187,7 @@ table td .grid-actions [role="button"] {
|
||||
color: var(--app-muted-color);
|
||||
border: 1px solid var(--app-border);
|
||||
font-size: small;
|
||||
background: var(--app-form-element-active-background-color);
|
||||
background: var(--app-form-element-background-color);
|
||||
box-shadow: var(--app-group-box-shadow);
|
||||
}
|
||||
|
||||
@@ -364,12 +372,51 @@ section {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
--app-font-size: 1.7rem;
|
||||
|
||||
}
|
||||
|
||||
h2 {
|
||||
--app-font-size: 1.6rem;
|
||||
|
||||
}
|
||||
|
||||
h3 {
|
||||
--app-font-size: 1.3rem;
|
||||
|
||||
}
|
||||
|
||||
h4 {
|
||||
--app-font-size: 1.2rem;
|
||||
|
||||
}
|
||||
|
||||
h5 {
|
||||
--app-font-size: 1.1rem;
|
||||
|
||||
}
|
||||
|
||||
h6 {
|
||||
--app-font-size: 1rem;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 700px;
|
||||
}
|
||||
.app-container.is-sidebar-hidden:has(.aside-icon-bar):has(.app-sidebar) {
|
||||
display: grid;
|
||||
min-height: 100dvh;
|
||||
align-items: flex-start;
|
||||
grid-template-columns: minmax(0, 60px) minmax(0, 1fr);
|
||||
}
|
||||
.sidebar-hidden .app-container:has(.aside-icon-bar):has(.app-sidebar) {
|
||||
grid-template-columns: minmax(0, 60px) minmax(0, 1fr);
|
||||
}
|
||||
.app-container:has(.aside-icon-bar):has(.app-sidebar) {
|
||||
display: grid;
|
||||
min-height: 100dvh;
|
||||
@@ -380,15 +427,15 @@ section {
|
||||
display: grid;
|
||||
min-height: 100dvh;
|
||||
align-items: flex-start;
|
||||
grid-template-columns: minmax(0, 60px) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 60px) minmax(0, 220px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.app-container.is-sidebar-collapsed {
|
||||
grid-template-columns: minmax(0, 60px) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 60px) minmax(0, 220px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sidebar-collapsed .app-container {
|
||||
grid-template-columns: minmax(0, 60px) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 60px) minmax(0, 220px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.app-main {
|
||||
@@ -404,6 +451,8 @@ section {
|
||||
.app-main-content:has(.app-details-container) {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,7 +683,6 @@ mark {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
text-decoration: none;
|
||||
@@ -665,7 +713,7 @@ del {
|
||||
--app-underline: var(--app-primary-underline);
|
||||
outline: 0;
|
||||
background-color: var(--app-background-color);
|
||||
color: var(--app-color);
|
||||
color: cornflowerblue;
|
||||
-webkit-text-decoration: var(--app-text-decoration);
|
||||
text-decoration: var(--app-text-decoration);
|
||||
text-decoration-color: var(--app-underline);
|
||||
@@ -2323,6 +2371,8 @@ details.dropdown > summary + ul li {
|
||||
padding: calc(var(--app-form-element-spacing-vertical) * 0.5)
|
||||
var(--app-form-element-spacing-horizontal);
|
||||
list-style: none;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--app-accordion-border-color);
|
||||
}
|
||||
|
||||
details.dropdown > summary + ul li:first-of-type {
|
||||
@@ -2331,6 +2381,7 @@ details.dropdown > summary + ul li:first-of-type {
|
||||
|
||||
details.dropdown > summary + ul li:last-of-type {
|
||||
margin-bottom: calc(var(--app-form-element-spacing-vertical) * 0.5);
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
details.dropdown > summary + ul li a {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
.app-sidebar {
|
||||
background: var(--app-sidebar-background-color);
|
||||
--app-sidebar-gap: 0.5rem;
|
||||
--app-sidebar-item-gap: 10px;
|
||||
--app-sidebar-title-size: 10px;
|
||||
--app-sidebar-summary-size: 12px;
|
||||
--app-sidebar-tenant-size: 14px;
|
||||
--app-sidebar-titlebar-height: 30px;
|
||||
--app-sidebar-border-width: 2px;
|
||||
--app-sidebar-summary-max-width: 157px;
|
||||
--app-sidebar-section-gap: 2rem;
|
||||
--app-sidebar-section-space: 5px;
|
||||
--app-sidebar-hover-bg: transparent;
|
||||
}
|
||||
|
||||
.app-sidebar .brand {
|
||||
@@ -7,13 +18,17 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--app-sidebar-gap);
|
||||
padding: 0 var(--app-spacing);
|
||||
margin-bottom: 0;
|
||||
min-height: var(--app-sidebar-titlebar-height);
|
||||
}
|
||||
|
||||
.aside-pending .app-sidebar-titlebar {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-panel {
|
||||
@@ -24,57 +39,62 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.app-sidebar.is-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .app-sidebar {
|
||||
.app-sidebar.is-collapsed .app-sidebar-panels,
|
||||
.app-sidebar.is-collapsed .app-sidebar-titlebar,
|
||||
.sidebar-collapsed .app-sidebar .app-sidebar-titlebar,
|
||||
.sidebar-collapsed .app-sidebar .app-sidebar-panels {
|
||||
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);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--app-sidebar-gap);
|
||||
padding: var(--app-spacing);
|
||||
}
|
||||
|
||||
|
||||
.app-sidebar .app-sidebar-tenant-logo a:hover {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-tenant-logo > a {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.app-sidebar-tenant-toggle {
|
||||
display: none;
|
||||
border: 1px solid var(--app-border);
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.app-sidebar-tenant-toggle:hover {
|
||||
color: var(--app-contrast);
|
||||
border-color: var(--app-contrast);
|
||||
}
|
||||
|
||||
.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);
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: var(--app-sidebar-tenant-size);
|
||||
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;
|
||||
font-size: var(--app-sidebar-title-size);
|
||||
color: var(--app-muted-color);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-sidebar-tools {
|
||||
@@ -102,6 +122,7 @@
|
||||
margin-bottom: 1rem;
|
||||
padding-block-start: 0.5rem;
|
||||
}
|
||||
|
||||
.app-sidebar-title {
|
||||
display: block;
|
||||
}
|
||||
@@ -113,38 +134,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
.app-sidebar a {
|
||||
color: inherit;
|
||||
display: flex;
|
||||
@media (max-width: 767px) {
|
||||
.app-sidebar-tenant-toggle {
|
||||
display: inline-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%;
|
||||
justify-content: center;
|
||||
}
|
||||
.app-sidebar-tenant-logo img {
|
||||
max-width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.app-sidebar {
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.app-sidebar-panels {
|
||||
padding-block-end: var(--app-spacing);
|
||||
}
|
||||
}
|
||||
|
||||
.app-sidebar a {
|
||||
color: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--app-sidebar-item-gap);
|
||||
text-decoration: none;
|
||||
padding: 5px var(--app-spacing);
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border-left: var(--app-sidebar-border-width) solid transparent;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-sidebar a:hover,
|
||||
.app-sidebar .active {
|
||||
.app-sidebar a.active,
|
||||
.app-sidebar .active > a {
|
||||
color: var(--app-primary);
|
||||
--app-background-color: inherit;
|
||||
background: var(--app-background-color);
|
||||
border-left: 2px solid var(--app-primary);
|
||||
background: var(--app-sidebar-hover-bg);
|
||||
border-left: var(--app-sidebar-border-width) solid var(--app-primary);
|
||||
}
|
||||
|
||||
aside.app-sidebar ul {
|
||||
aside.app-sidebar ul,
|
||||
aside.app-sidebar li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.app-sidebar small {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 5px;
|
||||
margin-top: var(--app-sidebar-section-gap);
|
||||
margin-bottom: var(--app-sidebar-section-space);
|
||||
display: block;
|
||||
padding-inline: var(--app-spacing);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group small {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
@@ -153,29 +196,20 @@ aside.app-sidebar ul {
|
||||
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;
|
||||
padding: 0 var(--app-spacing);
|
||||
font-size: var(--app-sidebar-summary-size);
|
||||
color: var(--app-muted-color);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
nav#aside-panel-people {
|
||||
margin-top: var(--app-spacing);
|
||||
margin-top: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group summary::-webkit-details-marker {
|
||||
@@ -188,21 +222,17 @@ nav#aside-panel-people {
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group summary > span {
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
max-width: 157px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
max-width: var(--app-sidebar-summary-max-width);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group details > ul li:first-child {
|
||||
margin-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.app-sidebar .app-sidebar-group details > ul li a {
|
||||
padding-block: 2px;
|
||||
opacity: .7;
|
||||
.app-sidebar .app-sidebar-group > small {
|
||||
padding-inline: var(--app-spacing);
|
||||
}
|
||||
.app-sidebar .app-sidebar-group details > ul li a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -1,29 +1,35 @@
|
||||
.app-header nav li {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form:has(input#app-search) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-header nav {
|
||||
gap: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-header {
|
||||
--app-topbar-item-padding: 5px;
|
||||
--app-topbar-list-gap: 8px;
|
||||
--app-topbar-divider: 1px solid var(--app-border);
|
||||
--app-topbar-margin-bottom: calc(var(--app-spacing) * 2);
|
||||
--app-topbar-muted: var(--app-form-element-placeholder-color);
|
||||
--app-topbar-bg: var(--app-background-color);
|
||||
padding-block-start: 0;
|
||||
padding-left: var(--app-spacing);
|
||||
padding-right: var(--app-spacing);
|
||||
margin-bottom: calc(var(--app-spacing) * 2);
|
||||
margin-bottom: var(--app-topbar-margin-bottom);
|
||||
color: var(--app-contrast);
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
border-bottom: var(--app-topbar-divider);
|
||||
}
|
||||
|
||||
main:has(.app-details-container) .app-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li:has(#toggle-main-content-aside) {
|
||||
.app-header nav {
|
||||
gap: var(--app-spacing);
|
||||
}
|
||||
|
||||
.app-header nav li {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-header form:has(input#app-search) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-header li:has(#toggle-main-content-aside) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -33,7 +39,7 @@ main:has(#app-details-aside-section) li:has(#toggle-main-content-aside) {
|
||||
|
||||
.app-header nav > ul {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: var(--app-topbar-list-gap);
|
||||
align-items: center;
|
||||
color: var(--app-muted-color);
|
||||
}
|
||||
@@ -41,7 +47,7 @@ main:has(#app-details-aside-section) li:has(#toggle-main-content-aside) {
|
||||
.app-header details.dropdown > summary:not([role]) {
|
||||
border: none;
|
||||
height: auto;
|
||||
padding: 5px;
|
||||
padding: var(--app-topbar-item-padding);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
@@ -51,15 +57,15 @@ main:has(#app-details-aside-section) li:has(#toggle-main-content-aside) {
|
||||
}
|
||||
|
||||
.app-header nav > ul li a {
|
||||
padding: 5px;
|
||||
color: var(--app-form-element-placeholder-color);
|
||||
padding: var(--app-topbar-item-padding);
|
||||
color: var(--app-topbar-muted);
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-header button {
|
||||
padding: 5px;
|
||||
color: var(--app-form-element-placeholder-color);
|
||||
padding: var(--app-topbar-item-padding);
|
||||
color: var(--app-topbar-muted);
|
||||
display: block;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
@@ -77,11 +83,11 @@ main:has(#app-details-aside-section) li:has(#toggle-main-content-aside) {
|
||||
header.app-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--app-background-color);
|
||||
background: var(--app-topbar-bg);
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.app-header details.dropdown[open] > summary::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
1
web/favicon/tenants
Symbolic link
1
web/favicon/tenants
Symbolic link
@@ -0,0 +1 @@
|
||||
../../storage/tenants
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 521 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1005 B |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -43,7 +43,7 @@ if ($defaultLocale !== null) {
|
||||
|
||||
// Auto-login from remember-me cookie
|
||||
if (empty($_SESSION['user']['id'])) {
|
||||
\MintyPHP\Service\RememberMeService::autoLoginFromCookie();
|
||||
\MintyPHP\Service\Auth\RememberMeService::autoLoginFromCookie();
|
||||
}
|
||||
|
||||
// Parse request URI and resolve locale
|
||||
@@ -84,7 +84,7 @@ if (!empty($_SESSION['user']['id']) && !$isAssetRequest) {
|
||||
$needsRefresh = true;
|
||||
}
|
||||
if ($needsRefresh) {
|
||||
\MintyPHP\Service\AuthService::loadTenantDataIntoSession($userId);
|
||||
\MintyPHP\Service\Auth\AuthService::loadTenantDataIntoSession($userId);
|
||||
$_SESSION['tenant_snapshot_at'] = time();
|
||||
}
|
||||
}
|
||||
@@ -115,10 +115,17 @@ if (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);
|
||||
if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) {
|
||||
trigger_error('MintyPHP template action"' . Router::getTemplateAction() . '" should not send output. Error raised ', E_USER_WARNING);
|
||||
}
|
||||
} else {
|
||||
ob_end_clean();
|
||||
}
|
||||
if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) {
|
||||
Session::end();
|
||||
DB::close();
|
||||
exit;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
if (Router::getAction()) {
|
||||
@@ -127,10 +134,17 @@ if (Router::getAction()) {
|
||||
}
|
||||
if (ob_get_contents()) {
|
||||
ob_end_flush();
|
||||
trigger_error('MintyPHP action "' . Router::getAction() . '" should not send output. Error raised ', E_USER_WARNING);
|
||||
if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) {
|
||||
trigger_error('MintyPHP action "' . Router::getAction() . '" should not send output. Error raised ', E_USER_WARNING);
|
||||
}
|
||||
} else {
|
||||
ob_end_clean();
|
||||
}
|
||||
if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) {
|
||||
Session::end();
|
||||
DB::close();
|
||||
exit;
|
||||
}
|
||||
|
||||
// End the session
|
||||
Session::end();
|
||||
|
||||
40
web/js/app-boot.js
Normal file
40
web/js/app-boot.js
Normal file
@@ -0,0 +1,40 @@
|
||||
(function () {
|
||||
var root = document.documentElement;
|
||||
root.classList.remove('no-js');
|
||||
root.classList.add('js');
|
||||
root.classList.add('aside-pending');
|
||||
|
||||
var assetBase = root.dataset.assetBase || '';
|
||||
if (assetBase) {
|
||||
window.APP_ASSET_BASE = assetBase;
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.localStorage.getItem('app-sidebar-collapsed') === '1') {
|
||||
root.classList.add('sidebar-collapsed');
|
||||
}
|
||||
if (window.localStorage.getItem('app-sidebar-hidden') === '1') {
|
||||
root.classList.add('sidebar-hidden');
|
||||
}
|
||||
var contrastValue = window.localStorage.getItem('app-contrast');
|
||||
if (contrastValue === 'high') {
|
||||
root.dataset.contrast = 'high';
|
||||
} else {
|
||||
root.dataset.contrast = 'normal';
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore storage errors
|
||||
}
|
||||
|
||||
if (typeof window.requestFsLightboxRefresh !== 'function') {
|
||||
window.requestFsLightboxRefresh = function () {
|
||||
if (typeof window.refreshFsLightbox === 'function') {
|
||||
window.refreshFsLightbox();
|
||||
window.__fsLightboxRefreshPending = false;
|
||||
return true;
|
||||
}
|
||||
window.__fsLightboxRefreshPending = true;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -7,15 +7,13 @@ 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 { initToolbarToggles } from './components/app-toggle-toolbar.js';
|
||||
import './components/app-filter-overflow.js';
|
||||
import './components/app-global-search.js';
|
||||
import './components/app-theme-toggle.js';
|
||||
import './components/app-contrast-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;
|
||||
}
|
||||
initToolbarToggles();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
/**
|
||||
* Async Flash Messages & Loading State
|
||||
* Renders flash messages into #async-messages for JS-triggered notifications
|
||||
@@ -46,7 +48,7 @@ const defaultTimeouts = {
|
||||
export function showAsyncFlash(type, message, timeout) {
|
||||
const container = document.getElementById('async-messages');
|
||||
if (!container) {
|
||||
console.warn('async-messages container not found');
|
||||
warnOnce('UI_EL_MISSING', 'Missing #async-messages container', { module: 'async-flash' });
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function bindBulkVisibility({
|
||||
containerSelector,
|
||||
buttonSelector,
|
||||
@@ -5,7 +7,15 @@ export function bindBulkVisibility({
|
||||
} = {}) {
|
||||
const container = document.querySelector(containerSelector);
|
||||
const buttons = Array.from(document.querySelectorAll(buttonSelector));
|
||||
if (!container || !buttons.length) return { update: () => {} };
|
||||
if (!container || !buttons.length) {
|
||||
if (!container) {
|
||||
warnOnce('UI_EL_MISSING', `Missing bulk container: ${containerSelector}`, { module: 'bulk-selection' });
|
||||
}
|
||||
if (!buttons.length) {
|
||||
warnOnce('UI_EL_MISSING', `Missing bulk buttons: ${buttonSelector}`, { module: 'bulk-selection' });
|
||||
}
|
||||
return { update: () => {} };
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const selected = container.querySelectorAll(selectedRowSelector).length > 0;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const toggles = document.querySelectorAll('[data-color-default-toggle]');
|
||||
|
||||
const syncToggle = (toggle) => {
|
||||
const targetSelector = toggle.dataset.colorTarget || '';
|
||||
if (!targetSelector) {
|
||||
warnOnce('UI_EL_MISSING', 'Missing data-color-target', { module: 'color-default-toggle' });
|
||||
return;
|
||||
}
|
||||
const target = document.querySelector(targetSelector);
|
||||
if (!target) {
|
||||
warnOnce('UI_EL_MISSING', `Missing color target: ${targetSelector}`, { module: 'color-default-toggle' });
|
||||
return;
|
||||
}
|
||||
const defaultValue = toggle.dataset.colorDefault || '#2fa4a4';
|
||||
@@ -27,6 +31,9 @@ const syncToggle = (toggle) => {
|
||||
toggles.forEach((toggle) => {
|
||||
const targetSelector = toggle.dataset.colorTarget || '';
|
||||
const target = targetSelector ? document.querySelector(targetSelector) : null;
|
||||
if (targetSelector && !target) {
|
||||
warnOnce('UI_EL_MISSING', `Missing color target: ${targetSelector}`, { module: 'color-default-toggle' });
|
||||
}
|
||||
|
||||
toggle.addEventListener('change', () => syncToggle(toggle));
|
||||
|
||||
|
||||
41
web/js/components/app-contrast-toggle.js
Normal file
41
web/js/components/app-contrast-toggle.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { requireEl } from '../core/app-dom.js';
|
||||
|
||||
const STORAGE_KEY = 'app-contrast';
|
||||
|
||||
const setContrast = (contrast) => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.contrast = contrast === 'high' ? 'high' : 'normal';
|
||||
};
|
||||
|
||||
const setIcon = (button, contrast) => {
|
||||
const icon = button.querySelector('i');
|
||||
if (!icon) return;
|
||||
icon.classList.remove('bi-circle-half', 'bi-highlights');
|
||||
icon.classList.add(contrast === 'high' ? 'bi-highlights' : 'bi-circle-half');
|
||||
};
|
||||
|
||||
const initContrastToggle = () => {
|
||||
const button = requireEl('[data-contrast-toggle]', { module: 'contrast-toggle' });
|
||||
if (!button) return;
|
||||
const root = document.documentElement;
|
||||
const getCurrent = () => (root.dataset.contrast === 'high' ? 'high' : 'normal');
|
||||
setIcon(button, getCurrent());
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
const current = getCurrent();
|
||||
const next = current === 'high' ? 'normal' : 'high';
|
||||
setContrast(next);
|
||||
setIcon(button, next);
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, next);
|
||||
} catch (e) {
|
||||
// ignore storage errors
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initContrastToggle);
|
||||
} else {
|
||||
initContrastToggle();
|
||||
}
|
||||
73
web/js/components/app-filter-overflow.js
Normal file
73
web/js/components/app-filter-overflow.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const getEmptyValues = (field) => {
|
||||
const own = field?.dataset?.filterEmpty || '';
|
||||
const parent = field?.closest?.('[data-filter-empty]')?.dataset?.filterEmpty || '';
|
||||
return [own, parent]
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const isFieldActive = (field) => {
|
||||
if (!field) return false;
|
||||
if (field.disabled) return false;
|
||||
if (field.type === 'hidden') return false;
|
||||
if (field.type === 'checkbox') return field.checked;
|
||||
if (field.tagName === 'SELECT' && field.multiple) {
|
||||
return Array.from(field.selectedOptions || []).some((option) => option.value);
|
||||
}
|
||||
if (field.tagName === 'SELECT') {
|
||||
const value = String(field.value || '').trim();
|
||||
const empty = new Set(['', 'all', ...getEmptyValues(field)]);
|
||||
return value !== '' && !empty.has(value);
|
||||
}
|
||||
return String(field.value || '').trim() !== '';
|
||||
};
|
||||
|
||||
const isOptionalActive = (item) => {
|
||||
const fields = item.matches('input, select, textarea')
|
||||
? [item]
|
||||
: Array.from(item.querySelectorAll('input, select, textarea'));
|
||||
return fields.some(isFieldActive);
|
||||
};
|
||||
|
||||
export function initFilterOverflow(root = document) {
|
||||
const toolbars = root.querySelectorAll('[data-filter-overflow]');
|
||||
toolbars.forEach((toolbar) => {
|
||||
if (toolbar.dataset.filterOverflowBound === '1') return;
|
||||
toolbar.dataset.filterOverflowBound = '1';
|
||||
|
||||
const optional = Array.from(toolbar.querySelectorAll('[data-filter-optional]'));
|
||||
if (!optional.length) return;
|
||||
|
||||
const toggle = toolbar.querySelector('[data-filter-toggle]');
|
||||
if (!toggle) {
|
||||
warnOnce('UI_EL_MISSING', 'Missing filter overflow toggle', { module: 'filter-overflow' });
|
||||
return;
|
||||
}
|
||||
|
||||
const setExpanded = (expanded) => {
|
||||
optional.forEach((item) => {
|
||||
item.hidden = !expanded;
|
||||
});
|
||||
toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
};
|
||||
|
||||
const hasActiveOptional = optional.some(isOptionalActive);
|
||||
setExpanded(hasActiveOptional);
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const expanded = toggle.getAttribute('aria-expanded') === 'true';
|
||||
setExpanded(!expanded);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initFilterOverflow());
|
||||
} else {
|
||||
initFilterOverflow();
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
const refreshFsLightbox = () => {
|
||||
if (typeof window.requestFsLightboxRefresh === 'function') {
|
||||
return window.requestFsLightboxRefresh();
|
||||
}
|
||||
if (typeof window.refreshFsLightbox === 'function') {
|
||||
window.refreshFsLightbox();
|
||||
window.__fsLightboxRefreshPending = false;
|
||||
return true;
|
||||
}
|
||||
window.__fsLightboxRefreshPending = true;
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -46,3 +51,7 @@ if (document.readyState === 'loading') {
|
||||
refreshFsLightbox();
|
||||
initObserver();
|
||||
}
|
||||
|
||||
if (window.__fsLightboxRefreshPending && typeof window.refreshFsLightbox === 'function') {
|
||||
refreshFsLightbox();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
const input = document.querySelector('#side-search');
|
||||
const resultsEl = document.querySelector('[data-global-search-results]');
|
||||
import { requireEl, optionalEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
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 input = requireEl('#side-search', { module: 'global-search' });
|
||||
const resultsEl = requireEl('[data-global-search-results]', { module: 'global-search' });
|
||||
|
||||
if (input && resultsEl) {
|
||||
const panel = optionalEl('#aside-panel-search');
|
||||
const toggleButton = optionalEl('[data-search-details-toggle]');
|
||||
const toggleIcon = optionalEl('[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 shortcutLabel = /(Mac|iPhone|iPad|iPod)/i.test(navigator.platform || navigator.userAgent)
|
||||
? '⌘K'
|
||||
: 'Ctrl+K';
|
||||
if (input.placeholder && !input.placeholder.includes('⌘') && !input.placeholder.includes('Ctrl+K')) {
|
||||
input.placeholder = `${input.placeholder} (${shortcutLabel})`;
|
||||
}
|
||||
|
||||
const itemsByKey = new Map();
|
||||
resultsEl.querySelectorAll('[data-search-key]').forEach((item) => {
|
||||
@@ -20,7 +25,7 @@ if (!input || !resultsEl) {
|
||||
if (key) itemsByKey.set(key, item);
|
||||
});
|
||||
if (itemsByKey.size === 0) {
|
||||
console.warn('[GlobalSearch] No [data-search-key] elements found in results container');
|
||||
warnOnce('UI_EL_MISSING', 'No [data-search-key] elements found in results container', { module: 'global-search' });
|
||||
}
|
||||
|
||||
const setLoadingState = (value) => {
|
||||
@@ -33,7 +38,7 @@ if (!input || !resultsEl) {
|
||||
items.forEach((item) => {
|
||||
const li = itemsByKey.get(item.key);
|
||||
if (!li) {
|
||||
console.warn('[GlobalSearch] No element found for key:', item.key);
|
||||
warnOnce('UI_EL_MISSING', `No element found for key: ${item.key}`, { module: 'global-search' });
|
||||
return;
|
||||
}
|
||||
const countEl = li.querySelector('[data-search-count]');
|
||||
@@ -54,7 +59,10 @@ if (!input || !resultsEl) {
|
||||
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);
|
||||
warnOnce('UI_DATA_MISSING', `Preview item missing url at index ${idx} for key: ${item.key}`, {
|
||||
module: 'global-search',
|
||||
item: previewItem
|
||||
});
|
||||
return;
|
||||
}
|
||||
const previewLi = document.createElement('li');
|
||||
@@ -95,7 +103,9 @@ if (!input || !resultsEl) {
|
||||
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);
|
||||
warnOnce('FETCH_FAILED', `Global search failed: ${response.status} ${response.statusText}`, {
|
||||
module: 'global-search'
|
||||
});
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
@@ -103,7 +113,10 @@ if (!input || !resultsEl) {
|
||||
if (Array.isArray(data)) {
|
||||
render(data, value);
|
||||
} else {
|
||||
console.warn('[GlobalSearch] Invalid response format, expected array but got:', typeof data, data);
|
||||
warnOnce('FETCH_INVALID', `Invalid response format for global search`, {
|
||||
module: 'global-search',
|
||||
type: typeof data
|
||||
});
|
||||
clear();
|
||||
}
|
||||
};
|
||||
@@ -117,7 +130,7 @@ if (!input || !resultsEl) {
|
||||
if (pending) window.clearTimeout(pending);
|
||||
pending = window.setTimeout(() => {
|
||||
fetchCounts(value).catch((err) => {
|
||||
console.warn('[GlobalSearch] Fetch error:', err);
|
||||
warnOnce('FETCH_ERROR', 'Global search fetch error', { module: 'global-search', err });
|
||||
clear();
|
||||
});
|
||||
}, 300);
|
||||
@@ -137,6 +150,39 @@ if (!input || !resultsEl) {
|
||||
submitSearch();
|
||||
});
|
||||
|
||||
const isEditableTarget = (target) => {
|
||||
if (!target) return false;
|
||||
if (target.isContentEditable) return true;
|
||||
const tag = target.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
};
|
||||
|
||||
const focusSearch = () => {
|
||||
if (!input) return;
|
||||
window.requestAnimationFrame(() => {
|
||||
input.focus();
|
||||
input.select();
|
||||
});
|
||||
};
|
||||
|
||||
const openSearch = () => {
|
||||
if (window.AppAsidePanels?.open) {
|
||||
window.AppAsidePanels.open('search');
|
||||
} else {
|
||||
document.querySelector('[data-aside-target="search"]')?.click();
|
||||
}
|
||||
focusSearch();
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!(event.metaKey || event.ctrlKey)) return;
|
||||
const key = event.key.toLowerCase();
|
||||
if (key !== 'k' && key !== '/') return;
|
||||
if (isEditableTarget(event.target) && event.target !== input) return;
|
||||
event.preventDefault();
|
||||
openSearch();
|
||||
});
|
||||
|
||||
const form = input.closest('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', (event) => {
|
||||
@@ -153,9 +199,6 @@ if (!input || !resultsEl) {
|
||||
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
|
||||
|
||||
98
web/js/components/app-multiselect-cascade.js
Normal file
98
web/js/components/app-multiselect-cascade.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { getMultiSelectInstance } from './app-multiselect-init.js';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const parseValueList = (value) =>
|
||||
String(value || '')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const setDisabledState = (option, disabled) => {
|
||||
option.dataset.disabled = disabled ? '1' : '0';
|
||||
option.classList.toggle('is-disabled', disabled);
|
||||
option.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
||||
option.style.pointerEvents = disabled ? 'none' : '';
|
||||
if (!option._cascadeGuard) {
|
||||
option.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
if (option.dataset.disabled === '1') {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
option._cascadeGuard = true;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelectAll = (instance, disabled) => {
|
||||
const selectAll = instance?.element?.querySelector('.multi-select-all');
|
||||
if (!selectAll) return;
|
||||
selectAll.classList.toggle('is-disabled', disabled);
|
||||
selectAll.setAttribute('aria-disabled', disabled ? 'true' : 'false');
|
||||
selectAll.style.pointerEvents = disabled ? 'none' : '';
|
||||
};
|
||||
|
||||
export const initMultiSelectCascade = ({
|
||||
parent,
|
||||
child,
|
||||
map = {},
|
||||
mode = 'disable',
|
||||
clearInvalid = true,
|
||||
}) => {
|
||||
const parentInput = typeof parent === 'string' ? document.querySelector(parent) : parent;
|
||||
const childInput = typeof child === 'string' ? document.querySelector(child) : child;
|
||||
if (!parentInput || !childInput) {
|
||||
if (!parentInput) {
|
||||
warnOnce('UI_EL_MISSING', `Missing multiselect parent: ${parent}`, { module: 'multiselect-cascade' });
|
||||
}
|
||||
if (!childInput) {
|
||||
warnOnce('UI_EL_MISSING', `Missing multiselect child: ${child}`, { module: 'multiselect-cascade' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const childInstance = getMultiSelectInstance(childInput);
|
||||
if (!childInstance) {
|
||||
warnOnce('UI_INIT_MISSING', 'Missing multiselect instance for child', { module: 'multiselect-cascade' });
|
||||
return;
|
||||
}
|
||||
|
||||
const apply = () => {
|
||||
const selectedParents = parseValueList(parentInput.value);
|
||||
const allowAll = selectedParents.length === 0;
|
||||
const allowed = new Set();
|
||||
|
||||
if (!allowAll) {
|
||||
selectedParents.forEach((id) => {
|
||||
const entries = map[id] || [];
|
||||
entries.forEach((entry) => allowed.add(String(entry)));
|
||||
});
|
||||
}
|
||||
|
||||
if (clearInvalid && !allowAll) {
|
||||
const selected = childInstance.selectedValues || [];
|
||||
selected.forEach((value) => {
|
||||
if (!allowed.has(String(value))) {
|
||||
childInstance.unselect(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const options = childInstance.element.querySelectorAll('.multi-select-option');
|
||||
let disabledCount = 0;
|
||||
options.forEach((option) => {
|
||||
const isAllowed = allowAll || allowed.has(String(option.dataset.value || ''));
|
||||
const disabled = mode === 'disable' ? !isAllowed : false;
|
||||
if (disabled) disabledCount += 1;
|
||||
setDisabledState(option, disabled);
|
||||
});
|
||||
|
||||
toggleSelectAll(childInstance, disabledCount > 0);
|
||||
};
|
||||
|
||||
parentInput.addEventListener('change', apply);
|
||||
apply();
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
let multiSelectLoader = null;
|
||||
|
||||
const resolveScriptUrl = (script) => {
|
||||
@@ -65,13 +67,14 @@ export const initMultiSelect = async (target = '[data-multi-select]') => {
|
||||
try {
|
||||
instance = new MultiSelect(element, options);
|
||||
} catch (error) {
|
||||
console.error('[multi-select] init failed', element, error);
|
||||
warnOnce('UI_INIT_FAIL', 'multi-select init failed', { module: 'multi-select', element, error });
|
||||
return;
|
||||
}
|
||||
if (element.dataset.disabled === 'true' || element.disabled) {
|
||||
instance.disable();
|
||||
}
|
||||
element.dataset.multiSelectInit = '1';
|
||||
element._multiSelectInstance = instance;
|
||||
instances.push(instance);
|
||||
if (syncInput) {
|
||||
syncInput._multiSelectInstance = instance;
|
||||
@@ -98,8 +101,22 @@ export const initMultiSelect = async (target = '[data-multi-select]') => {
|
||||
return instances;
|
||||
};
|
||||
|
||||
export const getMultiSelectInstance = (target) => {
|
||||
const element = typeof target === 'string' ? document.querySelector(target) : target;
|
||||
if (!element) return null;
|
||||
if (element._multiSelectInstance) return element._multiSelectInstance;
|
||||
const syncTarget = element.dataset?.syncTarget;
|
||||
if (syncTarget) {
|
||||
const syncElement = document.querySelector(syncTarget);
|
||||
if (syncElement && syncElement._multiSelectInstance) return syncElement._multiSelectInstance;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const autoInit = () => {
|
||||
initMultiSelect().catch(() => {});
|
||||
initMultiSelect().catch((error) => {
|
||||
warnOnce('UI_INIT_FAIL', 'multi-select auto init failed', { module: 'multi-select', error });
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
@@ -19,6 +19,29 @@ const initHistoryButtons = () => {
|
||||
const back = document.querySelector('#global-back');
|
||||
const forward = document.querySelector('#global-forward');
|
||||
|
||||
const isEditableTarget = (target) => {
|
||||
if (!target) return false;
|
||||
if (target.isContentEditable) return true;
|
||||
const tag = target.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!event.altKey || event.metaKey || event.ctrlKey) return;
|
||||
if (isEditableTarget(event.target)) return;
|
||||
if (event.key === 'ArrowLeft') {
|
||||
if (window.history.length > 1) {
|
||||
event.preventDefault();
|
||||
window.history.back();
|
||||
}
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
if (window.history.length > 1) {
|
||||
event.preventDefault();
|
||||
window.history.forward();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (back) {
|
||||
back.addEventListener('click', (event) => {
|
||||
if (window.history.length > 1) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import EditorJS from '../../vendor/editorjs/editorjs.mjs';
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const form = document.querySelector('[data-page-editor]');
|
||||
if (form) {
|
||||
const holder = form.querySelector('[data-editor-holder]');
|
||||
if (!holder) {
|
||||
// No holder available, skip initialization.
|
||||
warnOnce('UI_EL_MISSING', 'Missing editor holder', { module: 'page-editor' });
|
||||
} else {
|
||||
const contentField = form.querySelector('#page-content');
|
||||
if (!contentField) {
|
||||
warnOnce('UI_EL_MISSING', 'Missing #page-content input', { module: 'page-editor' });
|
||||
}
|
||||
const toggleButton = document.querySelector('[data-editor-toggle]');
|
||||
const saveButton = document.querySelector('[data-editor-save]');
|
||||
const canEdit = form.dataset.canEdit === '1';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const rules = {
|
||||
min: (value, min) => value.length >= min,
|
||||
upper: (value) => /[A-Z]/.test(value),
|
||||
@@ -25,6 +27,15 @@ export function initPasswordHints() {
|
||||
const passwordInput = passwordSelector ? document.querySelector(passwordSelector) : null;
|
||||
const confirmInput = confirmSelector ? document.querySelector(confirmSelector) : null;
|
||||
const emailInput = emailSelector ? document.querySelector(emailSelector) : null;
|
||||
if (passwordSelector && !passwordInput) {
|
||||
warnOnce('UI_EL_MISSING', `Missing password input: ${passwordSelector}`, { module: 'password-hints' });
|
||||
}
|
||||
if (confirmSelector && !confirmInput) {
|
||||
warnOnce('UI_EL_MISSING', `Missing confirm input: ${confirmSelector}`, { module: 'password-hints' });
|
||||
}
|
||||
if (emailSelector && !emailInput) {
|
||||
warnOnce('UI_EL_MISSING', `Missing email input: ${emailSelector}`, { module: 'password-hints' });
|
||||
}
|
||||
const minLength = Number.parseInt(container.dataset.minLength || '12', 10);
|
||||
const items = container.querySelectorAll('[data-rule]');
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const switchTenant = async (link) => {
|
||||
const tenantId = link.dataset.switchTenant;
|
||||
const csrfKey = link.dataset.csrfKey;
|
||||
@@ -5,7 +7,7 @@ const switchTenant = async (link) => {
|
||||
const errorMessage = link.dataset.errorMessage || 'Failed to switch tenant';
|
||||
|
||||
if (!tenantId || !csrfKey || !csrfToken) {
|
||||
console.error('Missing tenant ID or CSRF token');
|
||||
warnOnce('UI_DATA_MISSING', 'Missing tenant ID or CSRF token', { module: 'tenant-switcher' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,19 +36,19 @@ const switchTenant = async (link) => {
|
||||
if (data.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
console.error('Tenant switch failed:', data.error);
|
||||
warnOnce('FETCH_FAILED', 'Tenant switch failed', { module: 'tenant-switcher', error: data.error });
|
||||
alert(errorMessage);
|
||||
link.style.pointerEvents = '';
|
||||
link.style.opacity = '';
|
||||
}
|
||||
} else {
|
||||
console.error('Server error:', response.status);
|
||||
warnOnce('FETCH_FAILED', `Tenant switch server error: ${response.status}`, { module: 'tenant-switcher' });
|
||||
alert(errorMessage);
|
||||
link.style.pointerEvents = '';
|
||||
link.style.opacity = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
warnOnce('FETCH_ERROR', 'Tenant switch network error', { module: 'tenant-switcher', error });
|
||||
alert(errorMessage);
|
||||
link.style.pointerEvents = '';
|
||||
link.style.opacity = '';
|
||||
@@ -55,6 +57,10 @@ const switchTenant = async (link) => {
|
||||
|
||||
const initTenantSwitcher = () => {
|
||||
const tenantLinks = document.querySelectorAll('[data-switch-tenant]');
|
||||
if (!tenantLinks.length) {
|
||||
warnOnce('UI_EL_MISSING', 'Missing tenant switch links', { module: 'tenant-switcher' });
|
||||
return;
|
||||
}
|
||||
|
||||
tenantLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { optionalEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
const setTheme = (theme) => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = theme;
|
||||
document.documentElement.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 isDarkTheme = (theme) => theme && theme.startsWith('dark');
|
||||
|
||||
const setIcon = (iconEl, theme) => {
|
||||
if (!iconEl) return;
|
||||
iconEl.classList.remove('bi-sun-fill', 'bi-moon-stars-fill');
|
||||
iconEl.classList.add(isDarkTheme(theme) ? '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;
|
||||
const updateTheme = async (menu, theme) => {
|
||||
const url = menu.dataset.themeUrl;
|
||||
const csrfKey = menu.dataset.csrfKey;
|
||||
const csrfToken = menu.dataset.csrfToken;
|
||||
if (!url || !csrfKey || !csrfToken) {
|
||||
return true;
|
||||
}
|
||||
@@ -30,28 +32,78 @@ const updateTheme = async (button, theme) => {
|
||||
return response.ok;
|
||||
};
|
||||
|
||||
const initThemeMenu = () => {
|
||||
const menu = optionalEl('[data-theme-menu]');
|
||||
if (!menu) return;
|
||||
const icon = menu.querySelector('[data-theme-icon]');
|
||||
const options = Array.from(menu.querySelectorAll('[data-theme-option]'));
|
||||
if (!options.length) {
|
||||
warnOnce('UI_EL_MISSING', 'Missing theme options', { module: 'theme-toggle' });
|
||||
return;
|
||||
}
|
||||
|
||||
const getCurrent = () => document.documentElement.dataset.theme || '';
|
||||
const setActive = (theme) => {
|
||||
options.forEach((option) => {
|
||||
const isActive = option.dataset.themeValue === theme;
|
||||
option.classList.toggle('active', isActive);
|
||||
if (isActive) {
|
||||
option.setAttribute('aria-current', 'true');
|
||||
} else {
|
||||
option.removeAttribute('aria-current');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setIcon(icon, getCurrent());
|
||||
setActive(getCurrent());
|
||||
|
||||
options.forEach((option) => {
|
||||
option.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
const next = option.dataset.themeValue || '';
|
||||
if (!next) return;
|
||||
const current = getCurrent();
|
||||
if (next === current) return;
|
||||
setTheme(next);
|
||||
setIcon(icon, next);
|
||||
setActive(next);
|
||||
const ok = await updateTheme(menu, next);
|
||||
if (!ok) {
|
||||
setTheme(current);
|
||||
setIcon(icon, current);
|
||||
setActive(current);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initThemeToggle = () => {
|
||||
const button = document.querySelector('[data-theme-toggle]');
|
||||
const button = optionalEl('[data-theme-toggle]');
|
||||
if (!button) return;
|
||||
const root = document.documentElement;
|
||||
const getCurrent = () => (root.dataset.theme === 'dark' ? 'dark' : 'light');
|
||||
setIcon(button, getCurrent());
|
||||
const icon = button.querySelector('i');
|
||||
const getCurrent = () => document.documentElement.dataset.theme || '';
|
||||
setIcon(icon, getCurrent());
|
||||
|
||||
button.addEventListener('click', async () => {
|
||||
const current = getCurrent();
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
const next = isDarkTheme(current) ? 'light' : 'dark';
|
||||
setTheme(next);
|
||||
setIcon(button, next);
|
||||
setIcon(icon, next);
|
||||
const ok = await updateTheme(button, next);
|
||||
if (!ok) {
|
||||
setTheme(current);
|
||||
setIcon(button, current);
|
||||
setIcon(icon, current);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initThemeToggle);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initThemeMenu();
|
||||
initThemeToggle();
|
||||
});
|
||||
} else {
|
||||
initThemeMenu();
|
||||
initThemeToggle();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { requireEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function initAsidePanels(options = {}) {
|
||||
const {
|
||||
barSelector = '.aside-icon-bar',
|
||||
@@ -8,23 +10,38 @@ export function initAsidePanels(options = {}) {
|
||||
persistSidebarStateOnActivate = true
|
||||
} = options;
|
||||
|
||||
const bar = document.querySelector(barSelector);
|
||||
const bar = requireEl(barSelector, { module: 'aside-panels' });
|
||||
if (!bar) return;
|
||||
|
||||
const tabs = Array.from(bar.querySelectorAll('[data-aside-target]'));
|
||||
if (!tabs.length) return;
|
||||
if (!tabs.length) {
|
||||
warnOnce('UI_EL_MISSING', 'No aside tabs found', { module: 'aside-panels' });
|
||||
return;
|
||||
}
|
||||
const shortcutItems = Array.from(bar.querySelectorAll('[data-aside-target], [data-aside-shortcut]'));
|
||||
|
||||
const panels = Array.from(document.querySelectorAll(panelSelector));
|
||||
if (!panels.length) {
|
||||
warnOnce('UI_EL_MISSING', 'No aside panels found', { module: 'aside-panels' });
|
||||
}
|
||||
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 isMac = /(Mac|iPhone|iPad|iPod)/i.test(navigator.platform || navigator.userAgent);
|
||||
const shortcutPrefix = isMac ? '⌘' : 'Ctrl+';
|
||||
|
||||
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 isEditableTarget = (target) => {
|
||||
if (!target) return false;
|
||||
if (target.isContentEditable) return true;
|
||||
const tag = target.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
};
|
||||
|
||||
const initPanelDetails = (panel) => {
|
||||
if (!panel || panel.dataset.detailsBound === '1') return;
|
||||
@@ -101,6 +118,9 @@ export function initAsidePanels(options = {}) {
|
||||
|
||||
const openSidebar = (persist) => {
|
||||
if (!revealSidebarOnActivate || !sidebarController) return;
|
||||
if (typeof sidebarController.isHidden === 'function' && sidebarController.isHidden()) {
|
||||
sidebarController.show({ persist });
|
||||
}
|
||||
if (!sidebarController.isCollapsed()) return;
|
||||
sidebarController.open({ persist });
|
||||
};
|
||||
@@ -152,6 +172,19 @@ export function initAsidePanels(options = {}) {
|
||||
};
|
||||
|
||||
setActive(getInitialKey(), { fromUser: false });
|
||||
document.documentElement.classList.remove('aside-pending');
|
||||
window.AppAsidePanels = {
|
||||
setActive,
|
||||
open: (key) => setActive(key, { fromUser: true })
|
||||
};
|
||||
|
||||
shortcutItems.forEach((item, idx) => {
|
||||
const base = item.dataset.tooltipBase || item.getAttribute('data-tooltip') || item.getAttribute('aria-label') || '';
|
||||
if (!base) return;
|
||||
const shortcutKey = idx === 9 ? '0' : String(idx + 1);
|
||||
item.dataset.tooltipBase = base;
|
||||
item.setAttribute('data-tooltip', `${base} (${shortcutPrefix}${shortcutKey})`);
|
||||
});
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener('click', (event) => {
|
||||
@@ -176,12 +209,29 @@ export function initAsidePanels(options = {}) {
|
||||
return;
|
||||
}
|
||||
if (isActive && sidebarController) {
|
||||
sidebarController.close({ persist: true });
|
||||
if (sidebarController.isHidden?.()) {
|
||||
sidebarController.show({ persist: true });
|
||||
} else if (typeof sidebarController.hide === 'function') {
|
||||
sidebarController.hide({ persist: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setActive(key, { fromUser: true });
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!(event.metaKey || event.ctrlKey)) return;
|
||||
if (event.altKey) return;
|
||||
if (isEditableTarget(event.target)) return;
|
||||
const key = event.key;
|
||||
if (!/^[0-9]$/.test(key)) return;
|
||||
const index = key === '0' ? 9 : (Number.parseInt(key, 10) - 1);
|
||||
if (index < 0 || index >= shortcutItems.length) return;
|
||||
event.preventDefault();
|
||||
shortcutItems[index].click();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { optionalEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function initDetailsAsideToggle(options = {}) {
|
||||
const {
|
||||
buttonSelector = '#toggle-main-content-aside',
|
||||
@@ -6,12 +8,15 @@ export function initDetailsAsideToggle(options = {}) {
|
||||
storageKey = 'app-details-aside-collapsed'
|
||||
} = options;
|
||||
|
||||
const button = document.querySelector(buttonSelector);
|
||||
const aside = document.querySelector(asideSelector);
|
||||
const button = optionalEl(buttonSelector);
|
||||
const aside = optionalEl(asideSelector);
|
||||
if (!button || !aside) return;
|
||||
|
||||
const container = aside.closest(containerSelector) || document.querySelector(containerSelector);
|
||||
if (!container) return;
|
||||
if (!container) {
|
||||
warnOnce('UI_EL_MISSING', `Missing details container: ${containerSelector}`, { module: 'details-aside' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!button.getAttribute('aria-controls')) {
|
||||
button.setAttribute('aria-controls', aside.id || 'app-details-aside-section');
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { requireEl, warnOnce } from '../core/app-dom.js';
|
||||
|
||||
export function initSidebarToggle(options = {}) {
|
||||
const {
|
||||
buttonSelector = '#toggle-aside',
|
||||
buttonSelector = '[data-sidebar-toggle]',
|
||||
asideSelector = '#app-sidebar',
|
||||
containerSelector = '.app-container',
|
||||
storageKey = 'app-sidebar-collapsed'
|
||||
storageKey = 'app-sidebar-collapsed',
|
||||
hiddenStorageKey = 'app-sidebar-hidden'
|
||||
} = options;
|
||||
|
||||
const aside = document.querySelector(asideSelector);
|
||||
const aside = requireEl(asideSelector, { module: 'sidebar-toggle' });
|
||||
if (!aside) return null;
|
||||
|
||||
const buttons = Array.from(document.querySelectorAll(buttonSelector));
|
||||
if (!buttons.length) {
|
||||
warnOnce('UI_EL_MISSING', `Missing sidebar toggle buttons: ${buttonSelector}`, { module: 'sidebar-toggle' });
|
||||
}
|
||||
const container = document.querySelector(containerSelector);
|
||||
const root = document.documentElement;
|
||||
|
||||
@@ -21,6 +27,14 @@ export function initSidebarToggle(options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const readHiddenStored = () => {
|
||||
try {
|
||||
return window.localStorage.getItem(hiddenStorageKey);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeStored = (collapsed) => {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, collapsed ? '1' : '0');
|
||||
@@ -29,6 +43,14 @@ export function initSidebarToggle(options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const writeHiddenStored = (hidden) => {
|
||||
try {
|
||||
window.localStorage.setItem(hiddenStorageKey, hidden ? '1' : '0');
|
||||
} catch (error) {
|
||||
// ignore storage errors
|
||||
}
|
||||
};
|
||||
|
||||
const applyState = (collapsed, { persist = false } = {}) => {
|
||||
aside.classList.toggle('is-collapsed', collapsed);
|
||||
if (container) {
|
||||
@@ -48,7 +70,27 @@ export function initSidebarToggle(options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const applyHidden = (hidden, { persist = false } = {}) => {
|
||||
aside.classList.toggle('is-hidden', hidden);
|
||||
if (container) {
|
||||
container.classList.toggle('is-sidebar-hidden', hidden);
|
||||
}
|
||||
if (root) {
|
||||
root.classList.toggle('sidebar-hidden', hidden);
|
||||
}
|
||||
buttons.forEach((button) => {
|
||||
if (!button.getAttribute('aria-controls')) {
|
||||
button.setAttribute('aria-controls', aside.id || 'app-sidebar');
|
||||
}
|
||||
button.setAttribute('aria-expanded', hidden ? 'false' : button.getAttribute('aria-expanded') || 'true');
|
||||
});
|
||||
if (persist) {
|
||||
writeHiddenStored(hidden);
|
||||
}
|
||||
};
|
||||
|
||||
const isCollapsed = () => aside.classList.contains('is-collapsed');
|
||||
const isHidden = () => aside.classList.contains('is-hidden');
|
||||
|
||||
const getInitialState = () => {
|
||||
const stored = readStored();
|
||||
@@ -60,17 +102,31 @@ export function initSidebarToggle(options = {}) {
|
||||
|
||||
const controller = {
|
||||
isCollapsed,
|
||||
isHidden,
|
||||
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 })
|
||||
apply: (collapsed, { persist = false } = {}) => applyState(!!collapsed, { persist }),
|
||||
show: ({ persist = true } = {}) => applyHidden(false, { persist }),
|
||||
hide: ({ persist = true } = {}) => applyHidden(true, { persist }),
|
||||
toggleVisibility: ({ persist = true } = {}) => applyHidden(!isHidden(), { persist }),
|
||||
applyVisibility: (hidden, { persist = false } = {}) => applyHidden(!!hidden, { persist })
|
||||
};
|
||||
|
||||
applyState(getInitialState(), { persist: false });
|
||||
const hiddenStored = readHiddenStored();
|
||||
if (hiddenStored === '1' || hiddenStored === '0') {
|
||||
applyHidden(hiddenStored === '1', { persist: false });
|
||||
}
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const action = (button.dataset.sidebarAction || '').toLowerCase();
|
||||
if (action === 'visibility') {
|
||||
controller.toggleVisibility({ persist: true });
|
||||
return;
|
||||
}
|
||||
controller.toggle({ persist: true });
|
||||
});
|
||||
});
|
||||
@@ -82,12 +138,32 @@ export function initSidebarToggle(options = {}) {
|
||||
if (action === 'open') controller.open({ persist });
|
||||
if (action === 'close') controller.close({ persist });
|
||||
if (action === 'toggle') controller.toggle({ persist });
|
||||
if (action === 'show') controller.show({ persist });
|
||||
if (action === 'hide') controller.hide({ persist });
|
||||
if (action === 'toggle-visibility') controller.toggleVisibility({ persist });
|
||||
});
|
||||
|
||||
window.AppSidebar = controller;
|
||||
return controller;
|
||||
}
|
||||
|
||||
const isEditableTarget = (target) => {
|
||||
if (!target) return false;
|
||||
if (target.isContentEditable) return true;
|
||||
const tag = target.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!(event.metaKey || event.ctrlKey)) return;
|
||||
if (event.altKey) return;
|
||||
if (event.key.toLowerCase() !== 'b') return;
|
||||
if (isEditableTarget(event.target)) return;
|
||||
if (!window.AppSidebar || typeof window.AppSidebar.toggleVisibility !== 'function') return;
|
||||
event.preventDefault();
|
||||
window.AppSidebar.toggleVisibility({ persist: true });
|
||||
});
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => initSidebarToggle());
|
||||
} else {
|
||||
|
||||
27
web/js/core/app-dom.js
Normal file
27
web/js/core/app-dom.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const warned = new Set();
|
||||
|
||||
export const warnOnce = (code, message, details = {}) => {
|
||||
const key = `${code}:${message}`;
|
||||
if (warned.has(key)) return;
|
||||
warned.add(key);
|
||||
console.warn(`[${code}] ${message}`, details);
|
||||
};
|
||||
|
||||
export const requireEl = (selector, details = {}) => {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) {
|
||||
warnOnce('UI_EL_MISSING', `Missing element: ${selector}`, details);
|
||||
return null;
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
export const requireAll = (selector, details = {}) => {
|
||||
const list = Array.from(document.querySelectorAll(selector));
|
||||
if (!list.length) {
|
||||
warnOnce('UI_EL_MISSING', `Missing elements: ${selector}`, details);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
export const optionalEl = (selector) => document.querySelector(selector);
|
||||
@@ -1,3 +1,5 @@
|
||||
import { warnOnce } from './app-dom.js';
|
||||
|
||||
export function createServerGrid(options) {
|
||||
const {
|
||||
container,
|
||||
@@ -22,6 +24,12 @@ export function createServerGrid(options) {
|
||||
const gridjsLib = gridjs || window.gridjs;
|
||||
const containerEl = typeof container === 'string' ? document.querySelector(container) : container;
|
||||
if (!containerEl || !gridjsLib) {
|
||||
if (!containerEl) {
|
||||
warnOnce('UI_EL_MISSING', `Missing grid container: ${container}`, { module: 'grid-factory' });
|
||||
}
|
||||
if (!gridjsLib) {
|
||||
warnOnce('UI_LIB_MISSING', 'Grid.js not available', { module: 'grid-factory' });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
export const buildUrl = (appBase, path) => new URL(path, appBase).toString();
|
||||
|
||||
export const getAppBase = () => {
|
||||
const baseEl = document.querySelector('base');
|
||||
if (baseEl && baseEl.href) {
|
||||
return baseEl.href;
|
||||
}
|
||||
return `${window.location.origin}/`;
|
||||
};
|
||||
|
||||
export const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[char]));
|
||||
|
||||
export const buildBadgeList = (items, options = {}) => {
|
||||
const {
|
||||
variant = 'neutral',
|
||||
primaryVariant = 'info',
|
||||
primaryTooltip = ''
|
||||
} = options;
|
||||
|
||||
let list = items;
|
||||
let primary = '';
|
||||
if (items && typeof items === 'object' && !Array.isArray(items)) {
|
||||
list = Array.isArray(items.items) ? items.items : [];
|
||||
primary = String(items.primary ?? '');
|
||||
}
|
||||
if (!Array.isArray(list) || list.length === 0) return '';
|
||||
|
||||
const tooltipAttr = primaryTooltip
|
||||
? ` data-tooltip="${escapeHtml(primaryTooltip)}" data-tooltip-pos="top"`
|
||||
: '';
|
||||
|
||||
const badges = list.map((label) => {
|
||||
const labelText = String(label ?? '');
|
||||
const isPrimary = primary !== '' && labelText === primary;
|
||||
const badgeVariant = isPrimary ? primaryVariant : variant;
|
||||
const badgeTooltip = isPrimary && primaryTooltip ? tooltipAttr : '';
|
||||
return `<span class="badge" data-variant="${badgeVariant}"${badgeTooltip}>${escapeHtml(labelText)}</span>`;
|
||||
}).join(' ');
|
||||
|
||||
return `<div class="badge-list">${badges}</div>`;
|
||||
};
|
||||
|
||||
export const postAction = async (url, csrf, data = {}) => {
|
||||
const body = new URLSearchParams({ ...data, [csrf.key]: csrf.token });
|
||||
return fetch(url, {
|
||||
|
||||
Reference in New Issue
Block a user