feat: 1-to-1 message render + web data-lake backend
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user