big restructure

This commit is contained in:
2026-02-11 19:28:12 +01:00
parent cd59ccd99b
commit 3eb9cc0ac4
209 changed files with 5101 additions and 2459 deletions

View File

@@ -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"],

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[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, {