172 lines
3.6 KiB
Svelte
172 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import { folders } from "$lib/stores/folders.svelte";
|
|
|
|
interface Tab {
|
|
id: number | null;
|
|
title: string;
|
|
}
|
|
|
|
const tabs = $derived<Tab[]>([
|
|
{ id: null, title: "All Chats" },
|
|
...folders.list.map((folder) => ({
|
|
id: folder.folder_id,
|
|
title: folder.title,
|
|
})),
|
|
]);
|
|
|
|
const activeTab = $derived(
|
|
Math.max(
|
|
0,
|
|
tabs.findIndex((tab) => tab.id === folders.selectedId)
|
|
)
|
|
);
|
|
|
|
let containerEl = $state<HTMLDivElement>();
|
|
let indicatorEl = $state<HTMLDivElement>();
|
|
let clipPath = $state("");
|
|
|
|
function updateClipPath() {
|
|
const indicator = indicatorEl;
|
|
const activeEl = indicator?.children[activeTab] as HTMLElement | undefined;
|
|
if (!(indicator && activeEl) || indicator.offsetWidth === 0) {
|
|
return;
|
|
}
|
|
const { offsetLeft, offsetWidth } = activeEl;
|
|
const width = indicator.offsetWidth;
|
|
const left = ((offsetLeft / width) * 100).toFixed(1);
|
|
const right = (
|
|
((width - (offsetLeft + offsetWidth)) / width) *
|
|
100
|
|
).toFixed(1);
|
|
clipPath = `inset(0.25rem ${right}% 0.25rem ${left}% round var(--tab-radius))`;
|
|
}
|
|
|
|
$effect(() => {
|
|
const index = activeTab;
|
|
const total = tabs.length;
|
|
updateClipPath();
|
|
const baseEl =
|
|
total > 0
|
|
? (containerEl?.children[index] as HTMLElement | undefined)
|
|
: undefined;
|
|
baseEl?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
});
|
|
|
|
$effect(() => {
|
|
if (!indicatorEl) {
|
|
return;
|
|
}
|
|
const observer = new ResizeObserver(() => updateClipPath());
|
|
observer.observe(indicatorEl);
|
|
return () => observer.disconnect();
|
|
});
|
|
</script>
|
|
|
|
<div bind:this={containerEl} class="container" class:ready={clipPath !== ""}>
|
|
{#each tabs as tab (tab.id)}
|
|
<button type="button" class="tab" onclick={() => folders.select(tab.id)}>
|
|
{tab.title}
|
|
</button>
|
|
{/each}
|
|
|
|
<div
|
|
bind:this={indicatorEl}
|
|
class="active-indicator"
|
|
style={clipPath ? `clip-path: ${clipPath}` : undefined}
|
|
aria-hidden="true"
|
|
>
|
|
{#each tabs as tab (tab.id)}
|
|
<button type="button" class="tab" tabindex="-1">{tab.title}</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<style lang="scss">
|
|
.container,
|
|
.active-indicator {
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
flex-wrap: nowrap;
|
|
align-items: center;
|
|
|
|
padding-block: 0.375rem;
|
|
padding-inline: 0.25rem;
|
|
}
|
|
|
|
.container {
|
|
--tab-radius: 1.25rem;
|
|
|
|
user-select: none;
|
|
scrollbar-width: none;
|
|
|
|
position: relative;
|
|
|
|
overflow-x: auto;
|
|
|
|
border-radius: 1.5rem;
|
|
|
|
opacity: 0;
|
|
background-color: var(--color-background);
|
|
box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.1);
|
|
|
|
transition: opacity 150ms;
|
|
|
|
&::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
&.ready {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.active-indicator {
|
|
pointer-events: none;
|
|
will-change: clip-path;
|
|
|
|
isolation: isolate;
|
|
position: absolute;
|
|
z-index: 10;
|
|
inset: 0;
|
|
|
|
contain: layout style paint;
|
|
overflow: hidden;
|
|
|
|
width: fit-content;
|
|
|
|
background-color: var(--color-primary-opacity);
|
|
|
|
transition: clip-path var(--slide-transition);
|
|
}
|
|
|
|
.tab {
|
|
cursor: var(--custom-cursor, pointer);
|
|
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
gap: 0.25rem;
|
|
align-items: center;
|
|
|
|
padding: 0.375rem 1rem;
|
|
border: none;
|
|
border-radius: var(--tab-radius);
|
|
|
|
font-family: inherit;
|
|
font-size: 1rem;
|
|
font-weight: var(--font-weight-medium);
|
|
color: var(--color-text-secondary);
|
|
white-space: nowrap;
|
|
|
|
appearance: none;
|
|
background: none;
|
|
|
|
&:hover {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.active-indicator & {
|
|
color: var(--color-primary);
|
|
}
|
|
}
|
|
</style>
|