Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

264 lines
6.0 KiB
Svelte

<script lang="ts">
import { onMount } from "svelte";
let {
open = $bindable(true),
width,
position = $bindable<"left" | "right">("left"),
elem_classes = [],
elem_id = "",
onexpand = () => {},
oncollapse = () => {}
}: {
open?: boolean;
width: number | string;
position?: "left" | "right";
elem_classes?: string[];
elem_id?: string;
onexpand?: () => void;
oncollapse?: () => void;
} = $props();
// Using a temporary variable to animate the sidebar opening at the start
let mounted = $state(false);
let _open = $state(false);
let sidebar_div: HTMLElement;
let overlap_amount = $state(0);
let width_css = $derived(typeof width === "number" ? `${width}px` : width);
let prefersReducedMotion = $state(false);
// Check if the sidebar overlaps with the main content
function check_overlap(): void {
if (!sidebar_div.closest(".wrap")) return;
const parent_rect = sidebar_div.closest(".wrap")?.getBoundingClientRect();
if (!parent_rect) return;
const sidebar_rect = sidebar_div.getBoundingClientRect();
const available_space =
position === "left"
? parent_rect.left
: window.innerWidth - parent_rect.right;
overlap_amount = Math.max(0, sidebar_rect.width - available_space + 30);
}
onMount(() => {
sidebar_div.closest(".wrap")?.classList.add("sidebar-parent");
check_overlap();
window.addEventListener("resize", check_overlap);
const update_parent_overlap = (): void => {
document.documentElement.style.setProperty(
"--overlap-amount",
`${overlap_amount}px`
);
};
update_parent_overlap();
mounted = true;
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
prefersReducedMotion = mediaQuery.matches;
const updateMotionPreference = (e: MediaQueryListEvent): void => {
prefersReducedMotion = e.matches;
};
mediaQuery.addEventListener("change", updateMotionPreference);
return () => {
window.removeEventListener("resize", check_overlap);
mediaQuery.removeEventListener("change", updateMotionPreference);
};
});
// We need to wait for the component to be mounted before we can set the open state
// so that it animates correctly.
$effect(() => {
if (mounted) _open = open;
});
let _elem_classes = $derived(elem_classes?.join(" ") || "");
</script>
<div
class="sidebar {_elem_classes}"
id={elem_id}
class:open={_open}
class:right={position === "right"}
class:reduce-motion={prefersReducedMotion}
bind:this={sidebar_div}
style="width: {width_css}; {position}: calc({width_css} * -1)"
>
<button
onclick={() => {
_open = !_open;
open = _open;
if (_open) {
onexpand?.();
} else {
oncollapse?.();
}
}}
class="toggle-button"
aria-label="Toggle Sidebar"
>
<div class="chevron">
<span class="chevron-left"></span>
</div>
</button>
<div class="sidebar-content">
<slot />
</div>
</div>
<style>
/* Mobile styles (≤ 768px) */
@media (max-width: 768px) {
.sidebar {
width: 100vw !important;
}
.sidebar:not(.right) {
left: -100vw !important;
}
.sidebar.right {
right: -100vw !important;
}
.sidebar:not(.reduce-motion) {
transition: transform 0.3s ease-in-out !important;
}
:global(.sidebar-parent) {
padding-left: 0 !important;
padding-right: 0 !important;
}
:global(.sidebar-parent:has(.sidebar.open)) {
padding-left: 0 !important;
padding-right: 0 !important;
}
.sidebar.open {
z-index: 1001 !important;
}
}
:global(.sidebar-parent) {
display: flex !important;
padding-left: 0;
padding-right: 0;
}
:global(.sidebar-parent:not(.reduce-motion)) {
transition:
padding-left 0.3s ease-in-out,
padding-right 0.3s ease-in-out;
}
:global(.sidebar-parent:has(.sidebar.open:not(.right))) {
padding-left: var(--overlap-amount);
}
:global(.sidebar-parent:has(.sidebar.open.right)) {
padding-right: var(--overlap-amount);
}
.sidebar {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
height: 100%;
background-color: var(--background-fill-secondary);
transform: translateX(0%);
z-index: 1000;
}
.sidebar:not(.reduce-motion) {
transition: transform 0.3s ease-in-out;
}
.sidebar.open:not(.right) {
transform: translateX(100%);
box-shadow: var(--size-1) 0 var(--size-1) rgba(0, 0, 0, 0.05);
}
.sidebar.open.right {
transform: translateX(-100%);
box-shadow: calc(var(--size-1) * -1) 0 var(--size-1) rgba(0, 0, 0, 0.05);
}
.toggle-button {
position: absolute;
top: var(--size-4);
background: var(--background-fill-secondary);
border: 1px solid var(--border-color-primary);
cursor: pointer;
padding: var(--size-2);
display: flex;
align-items: center;
justify-content: center;
width: var(--size-7);
height: var(--size-8);
z-index: 1001;
border-radius: 0;
}
.toggle-button:not(.reduce-motion) {
transition: all 0.3s ease-in-out;
}
.sidebar:not(.right) .toggle-button {
left: 100%;
border-radius: 0 var(--size-8) var(--size-8) 0;
border-left: none;
}
.sidebar.right .toggle-button {
right: 100%;
transform: rotate(180deg);
border-radius: 0 var(--size-8) var(--size-8) 0;
border-left: none;
}
.open:not(.right) .toggle-button {
right: 0;
left: auto;
transform: rotate(180deg);
border-radius: 0 var(--size-8) var(--size-8) 0;
border-left: none;
border-right: 1px solid var(--border-color-primary);
}
.open.right .toggle-button {
left: 0;
right: auto;
transform: rotate(0deg);
border-radius: 0 var(--size-8) var(--size-8) 0;
border-left: none;
border-right: 1px solid var(--border-color-primary);
}
.chevron {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding-right: 8px;
}
.chevron-left {
position: relative;
width: var(--size-3);
height: var(--size-3);
border-top: var(--size-0-5) solid var(--body-text-color);
border-right: var(--size-0-5) solid var(--body-text-color);
transform: rotate(45deg);
}
.sidebar-content {
padding: var(--size-5);
padding-right: var(--size-8);
overflow-y: auto;
}
.sidebar.right .sidebar-content {
padding-left: var(--size-8);
}
</style>