🐶 Sync 2026-09-17 00:01:35

This commit is contained in:
github-actions[bot] committed 2026-09-17 00:01:35 +08:00
1 parent d0c748add2
commit 6c6ea07309
106 files changed
+7551 -5770

No files matched your search

+1 -1
View File
@@ -14,4 +14,4 @@ Restored HTML loses inline JS handlers (`innerHTML` serialization), so anything
**Bump `v` whenever the sidebar markup changes shape.**
Cross-document `@view-transition` rules live in `components/_view-transitions.css` (only `#sidebar` gets its own snapshot group — the topbar must not, or its top-layer snapshot escapes `.content-card`'s rounded-corner clipping during transitions); they assume the cache keeps the sidebar's first frame populated.
The theme captures no view transitions (`components/_view-transitions.css` sets `:root { view-transition-name: none }`); swapped and freshly loaded content fades in instead. On desktop, collapsing eases the rail's width (`_layout.css`) and `#main-wrapper`, a flex-1 sibling, follows it frame by frame: the content is pushed, never snapped and slid back. `sidebar-shadcn.js` sets `html[data-sidebar-anim]` on the first toggle so a page load never plays the transition.
+1 -1
View File
@@ -27,12 +27,12 @@
"@types/node": "^25.9.1",
"colorjs.io": "^0.6.1",
"husky": "^9.1.7",
"lightningcss": "^1.32.0",
"lint-staged": "^16.4.0",
"prettier": "^3.8.3",
"prettier-plugin-tailwindcss": "^0.8.0",
"tailwindcss": "^4.3.0",
"terser": "^5.47.1",
"tw-animate-css": "^1.4.0",
"vite": "^8.0.13"
},
"packageManager": "pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c"
+3 -8
View File
@@ -187,6 +187,9 @@ importers:
husky:
specifier: ^9.1.7
version: 9.1.7
lightningcss:
specifier: ^1.32.0
version: 1.32.0
lint-staged:
specifier: ^16.4.0
version: 16.4.0
@@ -202,9 +205,6 @@ importers:
terser:
specifier: ^5.47.1
version: 5.47.1
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
vite:
specifier: ^8.0.13
version: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0)
@@ -801,9 +801,6 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tw-animate-css@1.4.0:
resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
@@ -1300,8 +1297,6 @@ snapshots:
tslib@2.8.1:
optional: true
tw-animate-css@1.4.0: {}
undici-types@7.24.6: {}
vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0):
+27 -17
View File
@@ -1,10 +1,10 @@
/* ── Sidebar ──────────────────────────────────────────────────────── */
#sidebar {
/* Width only: `transition-all` would also ease background-color, making the
sidebar lag a beat behind the untransitioned content area on theme flips. */
@apply bg-sidebar-bg flex h-full w-[220px] shrink-0 flex-col overflow-hidden transition-[width] duration-200;
@apply max-md:fixed max-md:top-0 max-md:left-0 max-md:z-40 max-md:w-[240px] max-md:-translate-x-full max-md:shadow-xl max-md:transition-transform;
/* Mobile drawer: translate only; `transition-all` would also ease
background-color and lag the sidebar behind the content on theme flips. */
@apply bg-sidebar-bg flex h-full w-[220px] shrink-0 flex-col overflow-hidden;
@apply max-md:fixed max-md:top-0 max-md:left-0 max-md:z-40 max-md:w-[240px] max-md:-translate-x-full max-md:shadow-xl max-md:[transition:translate_.2s];
/* Drawer height tracks the dynamic viewport so its bottom edge meets the
Safari toolbar smoothly instead of jumping when the toolbar shows/hides. */
@apply max-md:h-dvh;
@@ -54,7 +54,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
}
.sidebar-toggle {
@apply text-sidebar-muted flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 transition-colors;
@apply text-sidebar-muted flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 [transition:color_.15s,background-color_.15s,border-color_.15s];
&:hover {
@apply bg-sidebar-accent text-sidebar-accent-fg;
@@ -82,7 +82,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
}
.sidebar-nav-parent {
@apply text-sidebar-muted flex w-full cursor-pointer appearance-none items-center justify-start gap-2.5 rounded-md border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium transition-colors duration-150;
@apply text-sidebar-muted flex w-full cursor-pointer appearance-none items-center justify-start gap-2.5 rounded-md border-0 bg-transparent px-2 py-1.5 text-left text-sm font-medium [transition:color_.15s,background-color_.15s,border-color_.15s];
&:hover {
@apply bg-sidebar-accent text-sidebar-accent-fg;
@@ -101,7 +101,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
}
.sidebar-chevron {
@apply ml-auto shrink-0 transition-transform duration-200;
@apply ml-auto shrink-0 [transition:rotate_.2s];
svg,
img {
@apply h-3.5 w-3.5;
@@ -132,7 +132,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
opening bounces every row below them. visibility transitions too, so the
collapsed links leave the tab order only once the close has finished. */
.sidebar-accordion-sub {
@apply invisible grid grid-rows-[0fr] overflow-hidden opacity-0 transition-[grid-template-rows,opacity,visibility] duration-200 ease-out;
@apply invisible grid grid-rows-[0fr] overflow-hidden opacity-0 [transition:grid-template-rows_.2s_var(--ease-out),opacity_.2s_var(--ease-out),visibility_.2s_var(--ease-out)];
> * {
@apply min-h-0 self-start;
@@ -156,7 +156,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
}
.sidebar-sub-link {
@apply text-sidebar-muted block rounded-md px-2 py-1.5 text-sm no-underline transition-colors;
@apply text-sidebar-muted block rounded-md px-2 py-1.5 text-sm no-underline [transition:color_.15s,background-color_.15s,border-color_.15s];
&:hover {
@apply bg-sidebar-accent text-sidebar-accent-fg;
@@ -178,7 +178,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
}
.sidebar-collapsed-popover-link {
@apply text-sidebar-muted mx-1 block rounded-sm px-2 py-1.5 text-sm no-underline transition-colors;
@apply text-sidebar-muted mx-1 block rounded-sm px-2 py-1.5 text-sm no-underline [transition:color_.15s,background-color_.15s,border-color_.15s];
&:hover {
@apply bg-sidebar-accent text-sidebar-accent-fg;
@@ -197,7 +197,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
}
.sidebar-logout {
@apply text-sidebar-muted flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm no-underline transition-colors;
@apply text-sidebar-muted flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm no-underline [transition:color_.15s,background-color_.15s,border-color_.15s];
&:hover {
@apply bg-sidebar-accent text-sidebar-accent-fg;
@@ -237,6 +237,12 @@ html[data-sidebar-collapsed="true"] #sidebar {
@apply border-panel-border bg-panel-bg flex flex-1 flex-col overflow-hidden rounded-xl border shadow-sm max-md:rounded-none max-md:border-none max-md:shadow-none;
}
/* The rail width eases and #main-wrapper (flex-1) follows it frame by frame.
data-sidebar-anim is set on the first toggle, so a page load never plays it. */
html[data-sidebar-anim] #sidebar {
@apply md:[transition:width_200ms_var(--ease-out)];
}
/* ── Card header ─────────────────────────────────────────────────── */
.card-header {
@@ -251,7 +257,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
}
.topbar-hamburger {
@apply text-muted-foreground hidden h-8 w-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 transition-colors max-md:flex;
@apply text-muted-foreground hidden h-8 w-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 [transition:color_.15s,background-color_.15s,border-color_.15s] max-md:flex;
&:hover {
@apply bg-muted;
@@ -283,7 +289,7 @@ html[data-sidebar-collapsed="true"] #sidebar {
}
button.topbar-icon-btn {
@apply text-muted-foreground inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 transition-colors;
@apply text-muted-foreground inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent p-0 [transition:color_.15s,background-color_.15s,border-color_.15s];
&:hover {
@apply bg-muted text-foreground;
@@ -295,16 +301,20 @@ button.topbar-icon-btn {
}
.topbar-indicators {
@apply flex items-center gap-2;
@apply flex items-center;
/* LuCI-owned spans: icon via ::before mask only (no JS / no img) */
span[data-indicator] {
> img {
@apply hidden;
}
@apply text-muted-foreground relative inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-[0px] transition-colors;
@apply text-muted-foreground relative inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-[0px] [transition:color_.15s,background-color_.15s,border-color_.15s,width_.15s_ease-out,opacity_.15s_ease-out,margin-left_.15s_ease-out] starting:w-0 starting:opacity-0;
@apply before:relative before:block before:size-4 before:shrink-0 before:bg-current before:[mask-size:cover] before:[mask-position:center] before:[mask-repeat:no-repeat];
& + span[data-indicator] {
@apply ml-2 starting:ml-0;
}
&:hover {
@apply bg-muted text-foreground;
}
@@ -335,7 +345,7 @@ button.topbar-icon-btn {
/* ── Maincontent ──────────────────────────────────────────────────── */
#maincontent {
@apply flex-1 overflow-y-auto p-6 max-md:p-4;
@apply flex-1 [scrollbar-gutter:stable] overflow-y-auto p-6 max-md:p-4;
h1 {
@apply mb-4 border-0 pl-0 text-2xl font-semibold tracking-tight;
@@ -409,7 +419,7 @@ button.topbar-icon-btn {
#sidebar-overlay {
/* Absolute, not fixed: iOS Safari 26 tints its bars from edge-touching
fixed elements' background-color, opacity notwithstanding. */
@apply bg-overlay-base-a50 pointer-events-none absolute inset-0 z-30 opacity-0 transition-opacity duration-200 md:hidden;
@apply bg-overlay-base-a50 pointer-events-none absolute inset-0 z-30 opacity-0 [transition:opacity_.2s] md:hidden;
&.active {
@apply pointer-events-auto opacity-100;
@@ -0,0 +1,14 @@
/* Everything still transitions, for 1ms: luci-base acts on transitionend
(dropdown focus, notification removal), which transition: none never fires. */
@media (prefers-reduced-motion: reduce) {
*,
::before,
::after {
@apply [transition-delay:0s]! [transition-duration:1ms]! [animation-delay:0s]! [animation-duration:1ms]!;
}
/* A loading spinner is state, not decoration. */
.spinning::before {
@apply [animation-duration:1s]!;
}
}
@@ -48,12 +48,24 @@
@apply whitespace-nowrap;
}
.fade-in {
@apply animate-in fade-in-0 fill-mode-backwards duration-400;
@keyframes shadcn-fade-in {
from {
opacity: 0;
}
}
/* Also content that lands after first paint: the view's top-level blocks
(full load and router swap; poll never re-inserts them) and a switched-in
form tab pane (the dashboard's polled tabs sit under .Dashboard). */
.fade-in,
#view > :not(.spinning),
#view > .cbi-map > .cbi-map-tabbed > [data-tab-active="true"],
.cbi-section-node-tabbed > [data-tab-active="true"] {
@apply animate-[shadcn-fade-in_250ms_ease_backwards];
}
.fade-out {
@apply pointer-events-none opacity-0 transition-opacity duration-400;
@apply pointer-events-none opacity-0 [transition:opacity_.4s];
}
.spinning {
@@ -16,7 +16,7 @@
}
.zonebadge {
@apply border-secondary bg-secondary text-secondary-foreground inline-flex items-center gap-1.5 overflow-visible rounded-md border px-2 py-1 align-middle text-sm leading-normal font-semibold transition-colors duration-200;
@apply border-secondary bg-secondary text-secondary-foreground inline-flex items-center gap-1.5 overflow-visible rounded-md border px-2 py-1 align-middle text-sm leading-normal font-semibold [transition:color_.2s,background-color_.2s,border-color_.2s];
&[style*="--zone-color-rgb"] {
@apply border-transparent bg-[rgb(var(--zone-color-rgb),.6)]!;
@@ -40,7 +40,7 @@
}
.ifacebadge {
@apply border-border bg-label-surface text-foreground inline-flex cursor-default flex-nowrap items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-sm font-normal transition-colors duration-200;
@apply border-border bg-label-surface text-foreground inline-flex cursor-default flex-nowrap items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-sm font-normal [transition:color_.2s,background-color_.2s,border-color_.2s];
.zonebadge > & {
@apply rounded-md px-1.5 py-0;
@@ -4,7 +4,7 @@ input[type="submit"],
input[type="reset"],
.btn,
.cbi-button {
@apply inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-colors duration-200;
@apply inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium [transition:color_.2s,background-color_.2s,border-color_.2s];
&[disabled] {
@apply cursor-not-allowed opacity-40;
@@ -64,7 +64,7 @@
}
.ifacebox-head {
@apply border-border bg-label-surface text-foreground w-full rounded-t-lg border-b px-3 py-2 text-center text-sm font-semibold transition-colors duration-200;
@apply border-border bg-label-surface text-foreground w-full rounded-t-lg border-b px-3 py-2 text-center text-sm font-semibold [transition:color_.2s,background-color_.2s,border-color_.2s];
&[style*="--zone-color-rgb"] {
@apply bg-[rgb(var(--zone-color-rgb),.65)]!;
@@ -1,5 +1,5 @@
.cbi-dropdown {
@apply relative inline-flex h-fit max-w-full min-w-fit shrink-0 items-center p-0 whitespace-nowrap transition-colors duration-200;
@apply relative inline-flex h-fit max-w-full min-w-fit shrink-0 items-center p-0 whitespace-nowrap [transition:color_.2s,background-color_.2s,border-color_.2s];
&[disabled] {
@apply pointer-events-none opacity-40;
@@ -13,8 +13,9 @@
}
}
/* luci-base moves focus into the list only on the ul's transitionend. */
> ul {
@apply m-0 flex flex-1 list-none flex-nowrap items-center gap-0.5 overflow-hidden;
@apply m-0 flex flex-1 list-none flex-nowrap items-center gap-0.5 overflow-hidden opacity-99 [transition:opacity_120ms_ease-out];
> li {
@apply m-0 hidden list-none items-center gap-1 overflow-hidden px-2 py-1.5;
@@ -48,7 +49,7 @@
}
&.dropdown {
@apply border-border bg-panel-bg absolute left-0 z-50 w-fit min-w-full overflow-y-auto rounded-lg border shadow-lg backdrop-blur-sm;
@apply border-border bg-panel-bg absolute left-0 z-50 w-fit min-w-full overflow-y-auto rounded-lg border opacity-100 shadow-lg;
> li {
@apply text-muted-foreground hover:bg-muted min-h-9 w-full cursor-pointer px-3 py-1.5 text-sm font-medium;
@@ -44,7 +44,7 @@
@apply inline-flex w-max max-w-96 flex-col items-start gap-3 max-md:max-w-full;
.item {
@apply border-border bg-panel-bg pointer-events-auto relative inline-flex cursor-move flex-col items-start gap-2 self-stretch overflow-hidden rounded-lg border py-3 pr-9 pl-4 break-all shadow-sm transition-all duration-200 select-text;
@apply border-border bg-panel-bg pointer-events-auto relative inline-flex cursor-move flex-col items-start gap-2 self-stretch overflow-hidden rounded-lg border py-3 pr-9 pl-4 break-all shadow-sm select-text [transition:scale_.2s,opacity_.2s,box-shadow_.2s,border-color_.2s,background-color_.2s];
&::after {
content: "×";
@@ -69,7 +69,7 @@
}
.add-item {
@apply border-border hover:border-border-a80 hover:bg-muted-a30 inline-flex items-center gap-1.5 self-stretch rounded-lg border border-dashed bg-transparent px-1 py-1 transition-colors duration-200;
@apply border-border hover:border-border-a80 hover:bg-muted-a30 inline-flex items-center gap-1.5 self-stretch rounded-lg border border-dashed bg-transparent px-1 py-1 [transition:color_.2s,background-color_.2s,border-color_.2s];
> input {
@apply min-w-56 flex-1;
@@ -8,7 +8,7 @@ input[type="number"],
input[type="email"],
.cbi-input-text,
.cbi-input {
@apply border-input bg-background text-foreground placeholder:text-muted-foreground-a70 focus:border-primary focus:ring-ring relative w-52.5 max-w-full rounded-lg border px-3 py-1.5 text-sm shadow-none transition-colors duration-200 focus:ring-2 focus:outline-none;
@apply border-input bg-background text-foreground placeholder:text-muted-foreground-a70 focus:border-primary focus:ring-ring relative w-52.5 max-w-full rounded-lg border px-3 py-1.5 text-sm shadow-none [transition:color_.2s,background-color_.2s,border-color_.2s] focus:ring-2 focus:outline-none;
.table.cbi-section-table & {
@apply w-full;
@@ -24,7 +24,7 @@ input[type="email"],
}
textarea {
@apply border-input bg-background text-foreground placeholder:text-muted-foreground-a70 focus:border-primary focus:ring-ring min-h-24 w-full resize-y rounded-lg border px-3 py-2 text-sm transition-colors duration-200 focus:ring-2 focus:outline-none;
@apply border-input bg-background text-foreground placeholder:text-muted-foreground-a70 focus:border-primary focus:ring-ring min-h-24 w-full resize-y rounded-lg border px-3 py-2 text-sm [transition:color_.2s,background-color_.2s,border-color_.2s] focus:ring-2 focus:outline-none;
&[disabled] {
@apply cursor-not-allowed opacity-40;
@@ -34,8 +34,8 @@ textarea {
input[type="radio"],
input[type="checkbox"] {
@apply relative mr-3 inline-block h-4 w-4 cursor-pointer appearance-none align-middle disabled:cursor-not-allowed;
@apply before:border-input before:bg-background before:absolute before:inset-0 before:rounded before:border before:transition-colors before:duration-200;
@apply after:bg-primary-foreground after:absolute after:inset-0 after:[mask-size:cover] after:[mask-position:center] after:[mask-repeat:no-repeat] after:opacity-0 after:transition-opacity after:duration-200;
@apply before:border-input before:bg-background before:absolute before:inset-0 before:rounded before:border before:[transition:background-color_.2s,border-color_.2s];
@apply after:bg-primary-foreground after:absolute after:inset-0 after:[mask-size:cover] after:[mask-position:center] after:[mask-repeat:no-repeat] after:opacity-0 after:[transition:opacity_.2s];
@apply checked:before:border-primary checked:before:bg-primary checked:after:opacity-100;
@apply focus:before:ring-ring focus:before:ring-2 focus:before:outline-none;
}
@@ -6,7 +6,7 @@
}
> .modal {
@apply border-border bg-panel-bg flex w-full max-w-2xl flex-col gap-4 rounded-xl border p-6 break-words whitespace-normal shadow-2xl backdrop-blur-lg;
@apply border-border bg-panel-bg flex w-full max-w-2xl flex-col gap-4 rounded-xl border p-6 break-words whitespace-normal shadow-2xl;
h4 {
@apply mb-0 text-center;
@@ -46,7 +46,7 @@ button.cmdk-trigger {
separates title from label — 460 read cramped once rows became
two-anchored. */
@apply border-panel-border bg-panel-bg mt-[15vh] flex w-[min(560px,calc(100vw-32px))] flex-col overflow-hidden rounded-xl border shadow-2xl;
@apply transition-[opacity,translate] duration-150 ease-out starting:-translate-y-1.5 starting:opacity-0;
@apply [transition:opacity_.15s_var(--ease-out),translate_.15s_var(--ease-out)] starting:-translate-y-1.5 starting:opacity-0;
@apply max-md:mt-0 max-md:h-dvh max-md:w-full max-md:rounded-none max-md:border-0 max-md:pt-[env(safe-area-inset-top)] max-md:shadow-none max-md:transition-none;
}
@@ -3,6 +3,6 @@
@apply before:text-foreground before:absolute before:inset-x-0 before:top-1/2 before:-translate-y-1/2 before:text-center before:text-[10px] before:leading-none before:whitespace-nowrap before:content-[attr(title)];
> div {
@apply from-progress-bar-start to-progress-bar-end h-full bg-gradient-to-r transition-all duration-300;
@apply from-progress-bar-start to-progress-bar-end h-full bg-gradient-to-r [transition:width_.3s];
}
}
@@ -6,7 +6,7 @@
DOM. Only width and opacity move. Reduced motion drops the transitions,
not the bar. */
#luci-nav-progress {
@apply from-progress-bar-start to-progress-bar-end pointer-events-none fixed top-0 left-0 z-80 h-0.5 w-0 bg-linear-to-r transition-[width] duration-300 ease-out;
@apply from-progress-bar-start to-progress-bar-end pointer-events-none fixed top-0 left-0 z-80 h-0.5 w-0 bg-linear-to-r [transition:width_.3s_var(--ease-out)];
&[data-state="done"] {
@apply opacity-0 [transition:width_.3s_ease-out,opacity_.15s_ease-in_.15s];
@@ -32,6 +32,6 @@
/* Outgoing page while the router renders the next one. */
#view.view-leaving {
@apply cursor-progress opacity-60 transition-opacity duration-150;
@apply cursor-progress opacity-60 [transition:opacity_.15s];
}
}
@@ -1,5 +1,5 @@
select {
@apply border-input bg-background text-foreground focus:border-primary focus:ring-ring max-w-full min-w-52.5 appearance-none rounded-lg border px-3 py-1.5 pr-9 text-sm transition-colors duration-200 focus:ring-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-40 md:max-w-100;
@apply border-input bg-background text-foreground focus:border-primary focus:ring-ring max-w-full min-w-52.5 appearance-none rounded-lg border px-3 py-1.5 pr-9 text-sm [transition:color_.2s,background-color_.2s,border-color_.2s] focus:ring-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-40 md:max-w-100;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238888a0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.6rem center;
@@ -34,7 +34,7 @@ table.table,
tr,
.tr {
@apply hover:bg-label-surface max-md:border-border transition-colors duration-200 max-md:flex max-md:flex-wrap max-md:border-b max-md:px-2 max-md:py-2;
@apply hover:bg-label-surface max-md:border-border [transition:background-color_.2s] max-md:flex max-md:flex-wrap max-md:border-b max-md:px-2 max-md:py-2;
&:first-child {
&:not([data-title]) {
@@ -74,7 +74,7 @@ table.table,
th,
.th {
@apply border-border bg-label-surface border-b px-3 py-2.5 text-left text-sm font-medium backdrop-blur-sm max-md:hidden;
@apply border-border bg-label-surface border-b px-3 py-2.5 text-left text-sm font-medium max-md:hidden;
&[width="100%"] {
@apply w-full;
@@ -14,7 +14,7 @@
}
> a {
@apply text-muted-foreground hover:text-foreground focus-visible:ring-ring-a50 inline-flex items-center justify-center rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap no-underline transition-all focus-visible:ring-[3px] focus-visible:outline-none;
@apply text-muted-foreground hover:text-foreground focus-visible:ring-ring-a50 inline-flex items-center justify-center rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap no-underline [transition:color_.15s,background-color_.15s,border-color_.15s,box-shadow_.15s] focus-visible:ring-[3px] focus-visible:outline-none;
}
&.cbi-tab > a {
@@ -39,7 +39,7 @@
@apply border-border relative flex items-center gap-0 overflow-x-auto border-b;
a {
@apply text-muted-foreground hover:text-foreground block px-6 py-3 text-center text-sm font-medium whitespace-nowrap transition-colors duration-200 hover:no-underline;
@apply text-muted-foreground hover:text-foreground block px-6 py-3 text-center text-sm font-medium whitespace-nowrap [transition:color_.2s,background-color_.2s,border-color_.2s] hover:no-underline;
}
> li.active {
@@ -7,7 +7,7 @@
`transition-all` would animate the inset and make the bubble fly in from
off-screen. `w-max` sizes the bubble by its content instead of the narrow
anchor's containing block (which squeezed it to one word per line). */
@apply border-border-a60 bg-panel-bg text-foreground absolute inset-s-[-10000px] z-50 w-max max-w-xs scale-95 rounded-lg border px-3 py-2 text-xs break-normal whitespace-normal opacity-0 shadow-lg backdrop-blur-sm transition-[visibility,opacity,scale] duration-200 ease-out;
@apply border-border-a60 bg-panel-bg text-foreground absolute inset-s-[-10000px] z-50 w-max max-w-xs scale-95 rounded-lg border px-3 py-2 text-xs break-normal whitespace-normal opacity-0 shadow-lg [transition:visibility_.2s_var(--ease-out),opacity_.2s_var(--ease-out),scale_.2s_var(--ease-out)];
&.error {
@apply border-destructive-a40 bg-destructive text-destructive-fg;
@@ -25,10 +25,10 @@
.cbi-tooltip-container,
[data-tooltip] {
@apply relative inline-block cursor-help whitespace-nowrap underline decoration-current decoration-dotted underline-offset-2 transition-all duration-200;
@apply relative inline-block cursor-help whitespace-nowrap underline decoration-current decoration-dotted underline-offset-2;
.cbi-tooltip {
@apply invisible scale-95 opacity-0 transition-[visibility,opacity,scale] duration-200 ease-out;
@apply invisible scale-95 opacity-0 [transition:visibility_.2s_var(--ease-out),opacity_.2s_var(--ease-out),scale_.2s_var(--ease-out)];
}
&:hover .cbi-tooltip {
@@ -1,23 +1,6 @@
/* Cross-document view transitions: same-origin full-page navigations
cross-fade instead of hard-flashing. Progressive enhancement — browsers
without support (e.g. Firefox) keep plain instant navigation. */
@view-transition {
navigation: auto;
}
/* The sidebar persists across pages, so it gets its own snapshot group and
stays visually static while only the rest of the page transitions. The
topbar deliberately has no group: its snapshot would render in the top
layer, unclipped by .content-card's rounded corners (squaring them for
the whole transition), and its pixels are identical across pages anyway
so the root cross-fade already keeps it visually static. */
#sidebar {
view-transition-name: shadcn-sidebar;
}
@media (prefers-reduced-motion: reduce) {
@view-transition {
navigation: none;
}
/* No view transitions: swapped and freshly loaded content fades in instead
(_utilities.css). A captured page would crossfade two layouts on top of
that fade, and a cross-document one would only fade to the spinner. */
:root {
view-transition-name: none;
}
+35 -9
View File
@@ -4,18 +4,44 @@
* Licensed to the public under the Apache License 2.0.
*/
@import "tailwindcss";
/* Theme variables and @apply'd utilities only: no Preflight, no scanned
classes. The reset below covers what sysauth.ut renders. */
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities) source(none);
@import "./inter.css";
@import "./_tokens.css";
@import "./_shared.css";
/* See main.css: keep dev-only tooling out of Tailwind's source detection. */
@source not "../../vite.config.ts";
@source not "../../scripts/*.client.js";
@custom-variant dark (&:where([data-darkmode=true], [data-darkmode=true] *));
@layer base {
*,
::before,
::after {
@apply box-border;
}
body,
p {
@apply m-0;
}
button,
input {
@apply m-0 border-0 bg-transparent p-0 [color:inherit] [font:inherit];
}
::placeholder {
@apply opacity-100;
}
[hidden] {
@apply hidden!;
}
}
html {
@apply bg-background text-foreground font-sans text-sm;
}
@@ -128,7 +154,7 @@ html {
/* Glass input wrapper — matches sign-in.tsx GlassInputWrapper */
.login-input-glass {
@apply border-border bg-foreground-a5 relative rounded-2xl border transition-colors;
@apply border-border bg-foreground-a5 relative rounded-2xl border [transition:color_.15s,background-color_.15s,border-color_.15s];
backdrop-filter: blur(4px);
&:focus-within {
@@ -152,7 +178,7 @@ html {
@apply absolute inset-y-0 right-0 flex cursor-pointer items-center border-0 bg-transparent px-3;
img {
@apply h-4 w-4 opacity-40 transition-opacity;
@apply h-4 w-4 opacity-40 [transition:opacity_.15s];
}
&:hover img {
@@ -161,7 +187,7 @@ html {
}
.login-submit {
@apply bg-primary text-primary-foreground hover:bg-primary-hover mt-4 w-full cursor-pointer rounded-2xl border-0 py-3.5 text-sm font-medium transition-colors;
@apply bg-primary text-primary-foreground hover:bg-primary-hover mt-4 w-full cursor-pointer rounded-2xl border-0 py-3.5 text-sm font-medium [transition:color_.15s,background-color_.15s,border-color_.15s];
}
/* ── Footer ── */
@@ -170,7 +196,7 @@ html {
@apply text-muted-foreground absolute right-0 bottom-5 left-0 px-6 text-center text-xs;
a {
@apply text-muted-foreground hover:text-foreground underline underline-offset-2 transition-colors;
@apply text-muted-foreground hover:text-foreground underline underline-offset-2 [transition:color_.15s,background-color_.15s,border-color_.15s];
}
}
+1 -1
View File
@@ -7,7 +7,6 @@
@import "tailwindcss";
@import "./inter.css";
@import "./_tokens.css";
@import "tw-animate-css";
/* Dev-only tooling holds prose and plain CSS whose bare words (static,
transition, backdrop-filter, …) Tailwind's automatic source detection would
@@ -41,3 +40,4 @@
@import "./components/_router.css";
@import "./_utilities.css";
@import "./_shared.css";
@import "./_reduced-motion.css";
@@ -522,11 +522,6 @@ return baseclass.extend({
];
const isMac = /Mac|iP(ad|hone|od)/.test(navigator.platform);
const keyEl = trigger.querySelector(".cmdk-trigger-key");
if (keyEl) {
keyEl.textContent = isMac ? "⌘K" : "Ctrl+K";
keyEl.hidden = false;
}
trigger.setAttribute("aria-keyshortcuts", isMac ? "Meta+K" : "Control+K");
trigger.addEventListener("click", () => this.openPalette());
@@ -116,6 +116,7 @@ return baseclass.extend({
"data-sidebar-collapsed",
next ? "true" : "false",
);
document.documentElement.setAttribute("data-sidebar-anim", "");
localStorage.setItem(SIDEBAR_KEY, next);
this._hideCollapsedPopover();
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import { readFileSync, statSync } from "node:fs";
import { resolve } from "node:path";
import test from "node:test";
const output = resolve(import.meta.dirname, "../../htdocs/luci-static");
const read = (path) => readFileSync(resolve(output, path), "utf8");
const bytes = (path) => statSync(resolve(output, path)).size;
test("pruned login.css keeps every consumed variable resolvable", () => {
const css = read("shadcn/login.css");
const declared = new Set(
[...css.matchAll(/[{;](--[\w-]+):/g)].map((m) => m[1]),
);
const registered = new Set(
[...css.matchAll(/@property\s+(--[\w-]+)/g)].map((m) => m[1]),
);
// sysauth.ut declares it inline at render time.
const injected = new Set(["--login-bg-image"]);
const unresolvable = [];
for (const [, name, delim] of css.matchAll(/var\(\s*(--[\w-]+)\s*([,)])/g))
if (
delim !== "," &&
!declared.has(name) &&
!registered.has(name) &&
!injected.has(name)
)
unresolvable.push(name);
assert.deepEqual([...new Set(unresolvable)], []);
for (const adminOnly of ["--sidebar-bg:", "--terminal-foreground:"])
assert.ok(!css.includes(adminOnly), `${adminOnly} should be pruned`);
assert.ok(
css.includes("-webkit-backdrop-filter"),
"prefixes must survive the prune",
);
});
// uhttpd serves identity bytes, so raw size is what every cold load pays.
// Set 2026-09 from main 129213, login 10521, menu 15859, router 15771,
// sidebar 6107, Inter latin 48256, logo 258.
test("production assets stay within raw-transfer budgets", () => {
const main = bytes("shadcn/main.css");
const login = bytes("shadcn/login.css");
const menu = bytes("resources/menu-shadcn.js");
const router = bytes("resources/router-shadcn.js");
const sidebar = bytes("resources/sidebar-shadcn.js");
const font = bytes("shadcn/inter-latin-wght-normal.woff2");
const logo = bytes("shadcn/images/logo.svg");
assert.ok(main <= 130_000, `main.css ${main} B exceeds 130 KB`);
assert.ok(login <= 11_000, `login.css ${login} B exceeds 11 KB`);
assert.ok(menu <= 16_000, `menu-shadcn.js ${menu} B exceeds 16 KB`);
assert.ok(router <= 16_000, `router-shadcn.js ${router} B exceeds 16 KB`);
assert.ok(sidebar <= 6_500, `sidebar-shadcn.js ${sidebar} B exceeds 6.5 KB`);
assert.ok(
main + menu + router + sidebar + font <= 216_000,
"admin assets exceed 216 KB",
);
assert.ok(login + font + logo <= 60_000, "login assets exceed 60 KB");
});
+64
View File
@@ -14,6 +14,7 @@ import { luciDev, injectMockBar } from "@eamonxg/luci-theme-devkit/vite/dev";
import { existsSync, readdirSync } from "fs";
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
import { dirname, join, resolve } from "path";
import { transform as lightningcssTransform } from "lightningcss";
import { minify as terserMinify } from "terser";
import { defineConfig, loadEnv, Plugin, ResolvedConfig } from "vite";
import config from "./luci-theme.config.js";
@@ -108,6 +109,68 @@ function createPatchAliasPlugin(): Plugin {
};
}
/* login.css imports the full token sheet but the login page consumes a
fraction of it. Drop every custom property (and --tw-* @property) no var()
can reach, following --a: var(--b) chains; consumers are never touched, so
sysauth.ut's inline --login-bg-image keeps working. Same as aurora's. */
function createLoginCssPrunePlugin(): Plugin {
return {
name: "login-css-prune",
apply: "build",
enforce: "post",
async closeBundle() {
const path = resolve(BUILD_OUTPUT, "shadcn/login.css");
if (!existsSync(path)) return;
const css = await readFile(path, "utf-8");
const VALUE = `(?:"[^"]*"|'[^']*'|[^;{}"'])*`;
const roots = new Set<string>();
const edges = new Map<string, Set<string>>();
for (const [, name, value] of css.matchAll(
new RegExp(`(?<=[{;])(--[\\w-]+|[a-zA-Z-]+):(${VALUE})`, "g"),
)) {
const refs = [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map(
(m) => m[1],
);
if (!name.startsWith("--")) refs.forEach((r) => roots.add(r));
else {
const deps = edges.get(name) ?? new Set<string>();
refs.forEach((r) => deps.add(r));
edges.set(name, deps);
}
}
const keep = new Set(roots);
const stack = [...roots];
while (stack.length) {
for (const dep of edges.get(stack.pop()!) ?? []) {
if (!keep.has(dep)) {
keep.add(dep);
stack.push(dep);
}
}
}
const pruned = css
.replace(
new RegExp(`(?<=[{;])(--[\\w-]+):${VALUE};?`, "g"),
(decl, name) => (keep.has(name) ? decl : ""),
)
.replace(/@property\s+(--[\w-]+)\{[^{}]*\}/g, (rule, name) =>
keep.has(name) ? rule : "",
)
.replace(/(?<=[{}])[^{}@]+\{\}/g, "");
// Parse only: a re-minify without Vite's targets would strip the
// -webkit- prefixes older Safari still needs.
lightningcssTransform({
filename: "login.css",
code: Buffer.from(pruned),
});
await writeFile(path, pruned);
},
};
}
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, CURRENT_DIR);
// VITE_OPENWRT_HOST is just the router address — a bare IP/hostname like
@@ -133,6 +196,7 @@ export default defineConfig(({ mode, command }) => {
...luciDev(config, { sshHost: OPENWRT_SSH_HOST }),
createLuciJsCompressPlugin(),
createPatchAliasPlugin(),
createLoginCssPrunePlugin(),
],
build: {
outDir: BUILD_OUTPUT,
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=A modern sidebar LuCI theme for OpenWrt, built with shadcn/ui design language.
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=0.5.4
PKG_RELEASE:=3
PKG_VERSION:=0.5.10
PKG_RELEASE:=4
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
@@ -1 +1 @@
"use strict";"require baseclass";"require ui";const ICON_MAP={status:"activity",system:"settings",network:"network",services:"layers",nas:"hard-drive",control:"sliders-horizontal",vpn:"shield",docker:"container",statistics:"chart-bar",nlbw:"gauge",asterisk:"phone",logout:"log-out",default:"layout-dashboard"},CACHE_KEY="shadcn.sidebar.cache",RECENTS_KEY="shadcn.palette.recents";return baseclass.extend({__init__(){ui.menu.load().then(e=>{this.renderSidebarChrome(),this.render(e),this.initUciIndicator(),this.cacheSidebar(),window.addEventListener("pagehide",()=>this.cacheSidebar()),document.dispatchEvent(new Event("shadcn-sidebar-ready"))})},cacheSidebar(){if(window.shadcnSuppressSidebarCache)return;const e=document.getElementById("sidebar"),t=document.getElementById("sidebar-nav");if(e&&t)try{sessionStorage.setItem(CACHE_KEY,JSON.stringify({v:1,lang:document.documentElement.lang||"",html:e.innerHTML,scroll:t.scrollTop||0}))}catch(e){}},_mediaBase(){const e=document.documentElement.getAttribute("data-shadcn-media"),t=document.getElementById("sidebar"),a=t&&t.getAttribute("data-media");return String(e||a||"/luci-static/shadcn").replace(/\/$/,"")},_sectionIcon(e,t){const a=String(e||"").toLowerCase(),s=ICON_MAP[a]||ICON_MAP.default;return this._iconFile(s,t)},_isLogoutMenuItem(e){const t=String(e&&null!=e.name?e.name:"").toLowerCase();return"logout"===t||t.endsWith("/logout")},_iconFile(e,t){const a=this._mediaBase();return E("img",{src:`${a}/icons/${e}.svg`,width:String(t),height:String(t),alt:"",loading:"lazy",class:"shadcn-icon"})},_resolveMenuBranch(e){const t=L.env.dispatchpath||[],a=L.env.requestpath||[],s=ui.menu.getChildren(e);let n=a.length&&a[0]||t.length&&t[0]||"admin",i=null;for(let e=0;e<s.length;e++)if(s[e].name===n){i=s[e];break}return i||1!==s.length||(i=s[0],n=i.name),i||(i=e),{branch:i,branchUrl:i.name||n}},renderSidebarChrome(){const e=document.getElementById("sidebar");if(!e)return;if("1"===e.getAttribute("data-shadcn-built")){const t=e.querySelector(".sidebar-brand-text"),a=e.getAttribute("data-hostname");return void(t&&a&&t.textContent!==a&&(t.textContent=a))}e.setAttribute("data-shadcn-built","1");const t=this._mediaBase(),a=e.getAttribute("data-hostname")||"Shadcn";for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(E("div",{class:"sidebar-header"},[E("a",{class:"sidebar-brand",href:"/"},[E("img",{class:"sidebar-logo",src:`${t}/images/logo.svg`,alt:"Shadcn",width:"24",height:"24"}),E("span",{class:"sidebar-brand-text"},[a])]),E("button",{id:"sidebar-toggle-btn",class:"sidebar-toggle",type:"button","aria-label":_("Navigation")},[E("span",{class:"icon-collapse"},[this._iconFile("panel-left-close",16)]),E("span",{class:"icon-expand"},[this._iconFile("panel-left-open",16)])])])),e.appendChild(E("nav",{class:"sidebar-nav",id:"sidebar-nav"})),e.appendChild(E("div",{class:"sidebar-footer",id:"sidebar-footer",hidden:""}))},render(e){const{branch:t,branchUrl:a}=this._resolveMenuBranch(e);this.tree=e,this.branch=t,this.branchUrl=a,this.renderSidebarNav(t,a),this.renderBreadcrumb(t,a),this.initPalette(t,a),this.renderTabs(e)},renderTabs(e){const t=L.env.dispatchpath||[],a=document.getElementById("tabmenu");a&&(a.innerHTML="",a.style.display="none");let s=e,n="";if(t.length>=3){for(var i=0;i<3&&s;i++){const e=t[i];if(!s.children||!s.children[e])break;s=s.children[e],n=n+(n?"/":"")+e}s&&this.renderTabMenu(s,n)}},syncRoute(){if(!this.tree)return;const e=document.getElementById("sidebar");if(e){e.querySelectorAll(".active").forEach(e=>e.classList.remove("active"));const t=new URL(L.url(...(L.env.dispatchpath||[]).slice(0,3)),location.href).pathname;let a=null,s=-1;if(e.querySelectorAll("a.sidebar-sub-link, a.sidebar-nav-parent, a.sidebar-logout").forEach(e=>{const n=e.pathname;(t===n||t.startsWith(n+"/"))&&n.length>s&&(a=e,s=n.length)}),a){const t=a.closest(".sidebar-sub-item");if(t){t.classList.add("active");const s=a.closest(".sidebar-accordion-item");s&&(s.classList.add("active"),"true"!==e.getAttribute("data-collapsed")&&e.querySelectorAll(".sidebar-accordion-item").forEach(e=>e.setAttribute("data-open",e===s?"true":"false")))}else a.classList.contains("sidebar-logout")?a.classList.add("active"):a.closest(".sidebar-nav-item")?.classList.add("active")}}this.renderBreadcrumb(this.branch,this.branchUrl),this.renderTabs(this.tree)},closeSurfaces(){this.closePalette(),window.ShadcnSidebar?.closeDrawer?.(),window.ShadcnSidebar?._hideCollapsedPopover?.()},renderSidebarNav(e,t){const a=document.getElementById("sidebar-nav"),s=document.getElementById("sidebar-footer");if(!a)return;const n=ui.menu.getChildren(e),i=L.env.dispatchpath||[],r="1"===document.getElementById("sidebar")?.getAttribute("data-shadcn-restored"),l=r?Array.from(a.querySelectorAll('.sidebar-accordion-item[data-open="true"]'),e=>e.getAttribute("data-section")):null,c=r?a.scrollTop:0;a.innerHTML="",s&&(s.innerHTML="",s.hidden=!0),n.forEach(e=>{if(this._isLogoutMenuItem(e)&&s){const a=i[1]==e.name,n=this._sectionIcon(e.name,18);returLine truncated
"use strict";"require baseclass";"require ui";const ICON_MAP={status:"activity",system:"settings",network:"network",services:"layers",nas:"hard-drive",control:"sliders-horizontal",vpn:"shield",docker:"container",statistics:"chart-bar",nlbw:"gauge",asterisk:"phone",logout:"log-out",default:"layout-dashboard"},CACHE_KEY="shadcn.sidebar.cache",RECENTS_KEY="shadcn.palette.recents";return baseclass.extend({__init__(){ui.menu.load().then(e=>{this.renderSidebarChrome(),this.render(e),this.initUciIndicator(),this.cacheSidebar(),window.addEventListener("pagehide",()=>this.cacheSidebar()),document.dispatchEvent(new Event("shadcn-sidebar-ready"))})},cacheSidebar(){if(window.shadcnSuppressSidebarCache)return;const e=document.getElementById("sidebar"),t=document.getElementById("sidebar-nav");if(e&&t)try{sessionStorage.setItem(CACHE_KEY,JSON.stringify({v:1,lang:document.documentElement.lang||"",html:e.innerHTML,scroll:t.scrollTop||0}))}catch(e){}},_mediaBase(){const e=document.documentElement.getAttribute("data-shadcn-media"),t=document.getElementById("sidebar"),a=t&&t.getAttribute("data-media");return String(e||a||"/luci-static/shadcn").replace(/\/$/,"")},_sectionIcon(e,t){const a=String(e||"").toLowerCase(),s=ICON_MAP[a]||ICON_MAP.default;return this._iconFile(s,t)},_isLogoutMenuItem(e){const t=String(e&&null!=e.name?e.name:"").toLowerCase();return"logout"===t||t.endsWith("/logout")},_iconFile(e,t){const a=this._mediaBase();return E("img",{src:`${a}/icons/${e}.svg`,width:String(t),height:String(t),alt:"",loading:"lazy",class:"shadcn-icon"})},_resolveMenuBranch(e){const t=L.env.dispatchpath||[],a=L.env.requestpath||[],s=ui.menu.getChildren(e);let n=a.length&&a[0]||t.length&&t[0]||"admin",i=null;for(let e=0;e<s.length;e++)if(s[e].name===n){i=s[e];break}return i||1!==s.length||(i=s[0],n=i.name),i||(i=e),{branch:i,branchUrl:i.name||n}},renderSidebarChrome(){const e=document.getElementById("sidebar");if(!e)return;if("1"===e.getAttribute("data-shadcn-built")){const t=e.querySelector(".sidebar-brand-text"),a=e.getAttribute("data-hostname");return void(t&&a&&t.textContent!==a&&(t.textContent=a))}e.setAttribute("data-shadcn-built","1");const t=this._mediaBase(),a=e.getAttribute("data-hostname")||"Shadcn";for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(E("div",{class:"sidebar-header"},[E("a",{class:"sidebar-brand",href:"/"},[E("img",{class:"sidebar-logo",src:`${t}/images/logo.svg`,alt:"Shadcn",width:"24",height:"24"}),E("span",{class:"sidebar-brand-text"},[a])]),E("button",{id:"sidebar-toggle-btn",class:"sidebar-toggle",type:"button","aria-label":_("Navigation")},[E("span",{class:"icon-collapse"},[this._iconFile("panel-left-close",16)]),E("span",{class:"icon-expand"},[this._iconFile("panel-left-open",16)])])])),e.appendChild(E("nav",{class:"sidebar-nav",id:"sidebar-nav"})),e.appendChild(E("div",{class:"sidebar-footer",id:"sidebar-footer",hidden:""}))},render(e){const{branch:t,branchUrl:a}=this._resolveMenuBranch(e);this.tree=e,this.branch=t,this.branchUrl=a,this.renderSidebarNav(t,a),this.renderBreadcrumb(t,a),this.initPalette(t,a),this.renderTabs(e)},renderTabs(e){const t=L.env.dispatchpath||[],a=document.getElementById("tabmenu");a&&(a.innerHTML="",a.style.display="none");let s=e,n="";if(t.length>=3){for(var i=0;i<3&&s;i++){const e=t[i];if(!s.children||!s.children[e])break;s=s.children[e],n=n+(n?"/":"")+e}s&&this.renderTabMenu(s,n)}},syncRoute(){if(!this.tree)return;const e=document.getElementById("sidebar");if(e){e.querySelectorAll(".active").forEach(e=>e.classList.remove("active"));const t=new URL(L.url(...(L.env.dispatchpath||[]).slice(0,3)),location.href).pathname;let a=null,s=-1;if(e.querySelectorAll("a.sidebar-sub-link, a.sidebar-nav-parent, a.sidebar-logout").forEach(e=>{const n=e.pathname;(t===n||t.startsWith(n+"/"))&&n.length>s&&(a=e,s=n.length)}),a){const t=a.closest(".sidebar-sub-item");if(t){t.classList.add("active");const s=a.closest(".sidebar-accordion-item");s&&(s.classList.add("active"),"true"!==e.getAttribute("data-collapsed")&&e.querySelectorAll(".sidebar-accordion-item").forEach(e=>e.setAttribute("data-open",e===s?"true":"false")))}else a.classList.contains("sidebar-logout")?a.classList.add("active"):a.closest(".sidebar-nav-item")?.classList.add("active")}}this.renderBreadcrumb(this.branch,this.branchUrl),this.renderTabs(this.tree)},closeSurfaces(){this.closePalette(),window.ShadcnSidebar?.closeDrawer?.(),window.ShadcnSidebar?._hideCollapsedPopover?.()},renderSidebarNav(e,t){const a=document.getElementById("sidebar-nav"),s=document.getElementById("sidebar-footer");if(!a)return;const n=ui.menu.getChildren(e),i=L.env.dispatchpath||[],r="1"===document.getElementById("sidebar")?.getAttribute("data-shadcn-restored"),l=r?Array.from(a.querySelectorAll('.sidebar-accordion-item[data-open="true"]'),e=>e.getAttribute("data-section")):null,c=r?a.scrollTop:0;a.innerHTML="",s&&(s.innerHTML="",s.hidden=!0),n.forEach(e=>{if(this._isLogoutMenuItem(e)&&s){const a=i[1]==e.name,n=this._sectionIcon(e.name,18);returLine truncated
@@ -1 +1 @@
"use strict";"require baseclass";const THEME_KEY="shadcn.theme",SIDEBAR_KEY="shadcn.sidebar";return baseclass.extend({__init__(){const e=document.getElementById("sidebar");"1"!==e?.getAttribute("data-shadcn-built")?document.addEventListener("shadcn-sidebar-ready",()=>this._start(),{once:!0}):this._start()},_start(){if(this._started)return;const e=document.getElementById("sidebar");"1"===e?.getAttribute("data-shadcn-built")&&(this._started=!0,this.initTheme(),this.initSidebar(),this.initMobileDrawer(),this.initThemeToggle())},initTheme(){const e=localStorage.getItem(THEME_KEY)||"device";this.applyTheme(e)},applyTheme(e){const t="dark"===e||"device"===e&&window.matchMedia("(prefers-color-scheme: dark)").matches;document.documentElement.setAttribute("data-darkmode",t),localStorage.setItem(THEME_KEY,e),this._updateThemeIcon(t)},_updateThemeIcon(e){const t=document.getElementById("theme-toggle-btn");if(!t)return;const o=t.querySelector("img");if(!o)return;const s=document.documentElement.getAttribute("data-shadcn-media")||"/luci-static/shadcn",d=String(s).replace(/\/$/,"");o.src=d+(e?"/icons/moon.svg":"/icons/sun.svg")},initThemeToggle(){const e=document.getElementById("theme-toggle-btn");e&&(e.addEventListener("click",()=>{const e="true"===document.documentElement.getAttribute("data-darkmode");this.applyTheme(e?"light":"dark")}),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{"device"===(localStorage.getItem(THEME_KEY)||"device")&&this.applyTheme("device")}))},initSidebar(){const e=document.getElementById("sidebar");if(!e)return;const t=getComputedStyle(document.documentElement).getPropertyValue("--breakpoint-md").trim();this._mobileQuery=window.matchMedia(`(width < ${t})`),this._mobileQuery.addEventListener("change",e=>{e.matches||this._hideCollapsedPopover()});const o="true"===localStorage.getItem(SIDEBAR_KEY);e.setAttribute("data-collapsed",o);const s=document.getElementById("sidebar-toggle-btn");s&&s.addEventListener("click",()=>{this._mobileQuery.matches?this.closeDrawer():this.toggleCollapse()}),this._initAccordion(),this._initCollapsedPopover()},toggleCollapse(){const e=document.getElementById("sidebar");if(!e)return;const t="true"!==e.getAttribute("data-collapsed");e.setAttribute("data-collapsed",t),document.documentElement.setAttribute("data-sidebar-collapsed",t?"true":"false"),localStorage.setItem(SIDEBAR_KEY,t),this._hideCollapsedPopover(),t&&document.querySelectorAll(".sidebar-accordion-item[data-open='true']").forEach(e=>{this._closeAccordionItem(e)})},_initCollapsedPopover(){document.addEventListener("mouseover",e=>{const t=document.getElementById("sidebar");if("true"!==t?.getAttribute("data-collapsed"))return;if(e.target.closest("#sidebar-collapsed-popover"))return void clearTimeout(this._popoverTimer);const o=e.target.closest(".sidebar-accordion-item");o&&(clearTimeout(this._popoverTimer),this._showCollapsedPopover(o))}),document.addEventListener("mouseout",e=>{const t=document.getElementById("sidebar");if("true"!==t?.getAttribute("data-collapsed"))return;if(!e.target.closest(".sidebar-accordion-item")&&!e.target.closest("#sidebar-collapsed-popover"))return;const o=e.relatedTarget;o&&(o.closest(".sidebar-accordion-item")||o.closest("#sidebar-collapsed-popover"))||(this._popoverTimer=setTimeout(()=>this._hideCollapsedPopover(),100))})},_showCollapsedPopover(e){const t=e.getAttribute("data-section")||"",o=document.getElementById("sidebar-collapsed-popover");if(o&&o.getAttribute("data-section")===t)return;this._hideCollapsedPopover();const s=e.querySelectorAll(".sidebar-sub-link");if(!s.length)return;const d=e.querySelector(".sidebar-label"),i=d?d.textContent.trim():"",r=document.createElement("div");if(r.id="sidebar-collapsed-popover",r.className="sidebar-collapsed-popover",r.setAttribute("data-section",t),i){const e=document.createElement("div");e.className="sidebar-collapsed-popover-title",e.textContent=i,r.appendChild(e)}const a=document.createElement("ul");a.className="sidebar-collapsed-popover-list",s.forEach(e=>{const t=e.closest(".sidebar-sub-item")?.classList.contains("active"),o=document.createElement("li"),s=document.createElement("a");s.href=e.href,s.className="sidebar-collapsed-popover-link"+(t?" active":""),s.textContent=e.textContent,o.appendChild(s),a.appendChild(o)}),r.appendChild(a),document.body.appendChild(r);const n=document.getElementById("sidebar").getBoundingClientRect(),c=e.getBoundingClientRect();r.style.left=n.right+4+"px";const l=Math.min(c.top,window.innerHeight-r.offsetHeight-8);r.style.top=Math.max(8,l)+"px"},_hideCollapsedPopover(){clearTimeout(this._popoverTimer);const e=document.getElementById("sidebar-collapsed-popover");e&&e.remove()},_initAccordion(){document.addEventListener("click",e=>{const t=e.target.closest(".sidebar-nav-parent");if(!t)return;const o=document.getElementById("sidebar");if("true"===o?.getAttribute("data-collapsed"))return;const s=t.closest(".sidebar-accordion-item");if(!s)return;const d="true"===s.getAttributLine truncated
"use strict";"require baseclass";const THEME_KEY="shadcn.theme",SIDEBAR_KEY="shadcn.sidebar";return baseclass.extend({__init__(){const e=document.getElementById("sidebar");"1"!==e?.getAttribute("data-shadcn-built")?document.addEventListener("shadcn-sidebar-ready",()=>this._start(),{once:!0}):this._start()},_start(){if(this._started)return;const e=document.getElementById("sidebar");"1"===e?.getAttribute("data-shadcn-built")&&(this._started=!0,this.initTheme(),this.initSidebar(),this.initMobileDrawer(),this.initThemeToggle())},initTheme(){const e=localStorage.getItem(THEME_KEY)||"device";this.applyTheme(e)},applyTheme(e){const t="dark"===e||"device"===e&&window.matchMedia("(prefers-color-scheme: dark)").matches;document.documentElement.setAttribute("data-darkmode",t),localStorage.setItem(THEME_KEY,e),this._updateThemeIcon(t)},_updateThemeIcon(e){const t=document.getElementById("theme-toggle-btn");if(!t)return;const o=t.querySelector("img");if(!o)return;const d=document.documentElement.getAttribute("data-shadcn-media")||"/luci-static/shadcn",s=String(d).replace(/\/$/,"");o.src=s+(e?"/icons/moon.svg":"/icons/sun.svg")},initThemeToggle(){const e=document.getElementById("theme-toggle-btn");e&&(e.addEventListener("click",()=>{const e="true"===document.documentElement.getAttribute("data-darkmode");this.applyTheme(e?"light":"dark")}),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{"device"===(localStorage.getItem(THEME_KEY)||"device")&&this.applyTheme("device")}))},initSidebar(){const e=document.getElementById("sidebar");if(!e)return;const t=getComputedStyle(document.documentElement).getPropertyValue("--breakpoint-md").trim();this._mobileQuery=window.matchMedia(`(width < ${t})`),this._mobileQuery.addEventListener("change",e=>{e.matches||this._hideCollapsedPopover()});const o="true"===localStorage.getItem(SIDEBAR_KEY);e.setAttribute("data-collapsed",o);const d=document.getElementById("sidebar-toggle-btn");d&&d.addEventListener("click",()=>{this._mobileQuery.matches?this.closeDrawer():this.toggleCollapse()}),this._initAccordion(),this._initCollapsedPopover()},toggleCollapse(){const e=document.getElementById("sidebar");if(!e)return;const t="true"!==e.getAttribute("data-collapsed");e.setAttribute("data-collapsed",t),document.documentElement.setAttribute("data-sidebar-collapsed",t?"true":"false"),document.documentElement.setAttribute("data-sidebar-anim",""),localStorage.setItem(SIDEBAR_KEY,t),this._hideCollapsedPopover(),t&&document.querySelectorAll(".sidebar-accordion-item[data-open='true']").forEach(e=>{this._closeAccordionItem(e)})},_initCollapsedPopover(){document.addEventListener("mouseover",e=>{const t=document.getElementById("sidebar");if("true"!==t?.getAttribute("data-collapsed"))return;if(e.target.closest("#sidebar-collapsed-popover"))return void clearTimeout(this._popoverTimer);const o=e.target.closest(".sidebar-accordion-item");o&&(clearTimeout(this._popoverTimer),this._showCollapsedPopover(o))}),document.addEventListener("mouseout",e=>{const t=document.getElementById("sidebar");if("true"!==t?.getAttribute("data-collapsed"))return;if(!e.target.closest(".sidebar-accordion-item")&&!e.target.closest("#sidebar-collapsed-popover"))return;const o=e.relatedTarget;o&&(o.closest(".sidebar-accordion-item")||o.closest("#sidebar-collapsed-popover"))||(this._popoverTimer=setTimeout(()=>this._hideCollapsedPopover(),100))})},_showCollapsedPopover(e){const t=e.getAttribute("data-section")||"",o=document.getElementById("sidebar-collapsed-popover");if(o&&o.getAttribute("data-section")===t)return;this._hideCollapsedPopover();const d=e.querySelectorAll(".sidebar-sub-link");if(!d.length)return;const s=e.querySelector(".sidebar-label"),i=s?s.textContent.trim():"",r=document.createElement("div");if(r.id="sidebar-collapsed-popover",r.className="sidebar-collapsed-popover",r.setAttribute("data-section",t),i){const e=document.createElement("div");e.className="sidebar-collapsed-popover-title",e.textContent=i,r.appendChild(e)}const a=document.createElement("ul");a.className="sidebar-collapsed-popover-list",d.forEach(e=>{const t=e.closest(".sidebar-sub-item")?.classList.contains("active"),o=document.createElement("li"),d=document.createElement("a");d.href=e.href,d.className="sidebar-collapsed-popover-link"+(t?" active":""),d.textContent=e.textContent,o.appendChild(d),a.appendChild(o)}),r.appendChild(a),document.body.appendChild(r);const n=document.getElementById("sidebar").getBoundingClientRect(),c=e.getBoundingClientRect();r.style.left=n.right+4+"px";const l=Math.min(c.top,window.innerHeight-r.offsetHeight-8);r.style.top=Math.max(8,l)+"px"},_hideCollapsedPopover(){clearTimeout(this._popoverTimer);const e=document.getElementById("sidebar-collapsed-popover");e&&e.remove()},_initAccordion(){document.addEventListener("click",e=>{const t=e.target.closest(".sidebar-nav-parent");if(!t)return;const o=document.getElementById("sidebar");if("true"===o?.getAttribute("data-collapsed"))return;const d=t.closest(".sidebaLine truncated
@@ -1,2 +1,2 @@
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-font-weight:initial;--tw-tracking:initial}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:.25rem;--breakpoint-md:48rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-sm:calc(var(--radius-base) * .5);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--radius-2xl:1rem;--radius-3xl:1.5rem;--shadow-sm:var(--shadow-sm);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--color-sidebar-fg:var(--sidebar-foreground);--color-terminal-fg:var(--terminal-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetLine truncated
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-2xl:1rem;--radius-3xl:1.5rem;}}@layer base{*,:before,:after{box-sizing:border-box}body,p{margin:calc(var(--spacing) * 0)}button,input{margin:calc(var(--spacing) * 0);border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);color:inherit;font:inherit;background-color:#0000;border-width:0}::placeholder{opacity:1}[hidden]{display:none!important}}@layer components,utilities;@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-cyrillic-wght-normal.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+1F??}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-greek-wght-normal.woff2)format("woff2-variations");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-vietnamese-wght-normal.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-ext-wght-normal.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/luci-static/shadcn/inter-latin-wght-normal.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--background:oklch(98.5% .004 275);--foreground:oklch(18% .02 275);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--destructive:oklch(55% .2 25);--border:oklch(89.65% .0058 275);--muted-foreground:oklch(52.62% .0131 275);--primary-hover:oklch(62.7% .158 275);--primary-a10:oklch(56.7% .158 275/.1);--primary-a20:oklch(56.7% .158 275/.2);--primary-a70:oklch(56.7% .158 275/.7);--destructive-a8:oklch(55% .2 25/.08);--destructive-a30:oklch(55% .2 25/.3);--foreground-a5:oklch(18% .02 275/.05);--font-sans:"Inter Variable", ui-sans-serif, system-ui, sans-serif;}[data-darkmode=true]{--background:oklch(15.9% .005 264);--foreground:oklch(90% .006 264);--primary:oklch(56.7% .158 275);--primary-foreground:oklch(100% 0 0);--destructive:oklch(63% .16 25);--border:oklch(26.27% .0051 264);--muted-foreground:oklch(58.14% .0056 264);--primary-hover:oklch(62.7% .158 275);--primary-a10:oklch(56.7% .158 275/.1);--primary-a20:oklch(56.7% .158 275/.2);--primary-a70:oklch(56.7% .158 275/.7);--destructive-a8:oklch(63% .16 25/.08);--destructive-a30:oklch(63% .16 25/.3);--foreground-a5:oklch(90% .006 264/.05);}html{color-scheme:light}html[data-darkmode=true]{color-scheme:dark}.card-header img,.login-input-icon img,.login-pw-toggle img,img.shadcn-icon{filter:brightness(0)saturate()}html[data-darkmode=true] .card-header img,html[data-darkmode=true] .login-input-icon img,html[data-darkmode=true] .login-pw-toggle img,html[data-darkmode=true] img.shadcn-icon{filter:brightness(0)saturate()invert()}.card-header img,img.shadcn-icon,#sidebar img.shadcn-icon{opacity:.65}html[data-darkmode=true] #sidebar img.shadcn-icon{opacity:.7}.card-header button:hover img,#sidebar .sidebar-nav-item.active img,#sidebar .sidebar-accordion-item.active img,#sidebar .sidebar-nav-parent:hover img{opacity:1}html{background-color:var(--background);font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--foreground)}@keyframes fadeSlideIn{to{opacity:1;transform:translateY(0)}}@keyframes slideRightIn{to{opacity:1;transfLine truncated
@@ -1,2 +1,2 @@
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-duration:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-ease:initial;--tw-content:"";--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-space-y-reverse:0;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:.25rem;--breakpoint-md:48rem;--container-xs:20rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base) * .5);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--shadow-sm:var(--shadow-sm);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--color-sidebar-fg:var(--sidebar-foreground);--color-terminal-fg:var(--terminal-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherLine truncated
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-content:"";--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-space-y-reverse:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:.25rem;--breakpoint-md:48rem;--container-xs:20rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:calc(var(--radius-base) * .5);--radius-md:calc(var(--radius-base) * .75);--radius-lg:var(--radius-base);--radius-xl:calc(var(--radius-base) * 1.5);--shadow-sm:var(--shadow-sm);--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:calc(var(--radius-base) * .5);--color-sidebar-fg:var(--sidebar-foreground);--color-terminal-fg:var(--terminal-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-Line truncated
@@ -195,7 +195,7 @@
aria-label="{{ _('Type to filter…') }}"
>
<span class="cmdk-trigger-label">{{ _('Type to filter…') }}</span>
<kbd class="cmdk-trigger-key" hidden></kbd>
<kbd class="cmdk-trigger-key">{{ match(http.getenv('HTTP_USER_AGENT') ?? '', /Mac|iP(ad|hone|od)/) ? '⌘K' : 'Ctrl+K' }}</kbd>
</button>
<div id="indicators" class="topbar-indicators"></div>
<button