mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-09-25 12:29:04 +02:00
feat: add tab bar navigation for crowded pages
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
>
|
||||
<div
|
||||
class="{!isAuthPage
|
||||
? 'max-w-410'
|
||||
? 'max-w-[1720px]'
|
||||
: ''} mx-auto flex w-full items-center justify-between px-4 md:px-10"
|
||||
>
|
||||
<div class="flex h-16 items-center">
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<TabsPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tabs-content"
|
||||
class={cn('text-sm flex-1 outline-none', className)}
|
||||
class={cn(
|
||||
'text-sm flex-1 outline-none data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-1 data-[state=active]:duration-200 motion-reduce:data-[state=active]:animate-none',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -19,22 +19,105 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
variant = 'default',
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: TabsPrimitive.ListProps & {
|
||||
variant?: TabsListVariant;
|
||||
} = $props();
|
||||
|
||||
let indicatorOffset = $state(0);
|
||||
let indicatorSize = $state(0);
|
||||
let indicatorVisible = $state(false);
|
||||
let indicatorOrientation = $state<'horizontal' | 'vertical'>('horizontal');
|
||||
|
||||
function updateIndicator() {
|
||||
if (!ref || variant !== 'line') {
|
||||
indicatorVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTrigger = ref.querySelector<HTMLElement>(
|
||||
'[data-slot="tabs-trigger"][data-state="active"]'
|
||||
);
|
||||
if (!activeTrigger) {
|
||||
indicatorVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const listRect = ref.getBoundingClientRect();
|
||||
const triggerRect = activeTrigger.getBoundingClientRect();
|
||||
indicatorOrientation = ref.dataset.orientation === 'vertical' ? 'vertical' : 'horizontal';
|
||||
|
||||
if (indicatorOrientation === 'vertical') {
|
||||
indicatorOffset = triggerRect.top - listRect.top;
|
||||
indicatorSize = triggerRect.height;
|
||||
} else {
|
||||
const triggerStyle = getComputedStyle(activeTrigger);
|
||||
const paddingLeft = Number.parseFloat(triggerStyle.paddingLeft);
|
||||
const paddingRight = Number.parseFloat(triggerStyle.paddingRight);
|
||||
indicatorOffset = triggerRect.left - listRect.left + paddingLeft;
|
||||
indicatorSize = triggerRect.width - paddingLeft - paddingRight;
|
||||
}
|
||||
indicatorVisible = true;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!ref || variant !== 'line') return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateIndicator);
|
||||
const observeTriggers = () => {
|
||||
ref
|
||||
?.querySelectorAll<HTMLElement>('[data-slot="tabs-trigger"]')
|
||||
.forEach((trigger) => resizeObserver.observe(trigger));
|
||||
updateIndicator();
|
||||
};
|
||||
const mutationObserver = new MutationObserver(observeTriggers);
|
||||
|
||||
resizeObserver.observe(ref);
|
||||
observeTriggers();
|
||||
mutationObserver.observe(ref, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-state'],
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.List
|
||||
bind:ref
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
class={cn(tabsListVariants({ variant }), className)}
|
||||
class={cn(tabsListVariants({ variant }), variant === 'line' && 'relative', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if variant === 'line'}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-slot="tabs-indicator"
|
||||
class={cn(
|
||||
'bg-foreground pointer-events-none absolute rounded-full opacity-0 transition-[transform,width,height,opacity] duration-300 ease-out motion-reduce:transition-none',
|
||||
indicatorOrientation === 'horizontal' ? 'bottom-0 left-0 h-0.5' : 'top-0 right-0 w-0.5',
|
||||
indicatorVisible && 'opacity-100'
|
||||
)}
|
||||
style:width={indicatorOrientation === 'horizontal' ? `${indicatorSize}px` : undefined}
|
||||
style:height={indicatorOrientation === 'vertical' ? `${indicatorSize}px` : undefined}
|
||||
style:transform={indicatorOrientation === 'horizontal'
|
||||
? `translate3d(${indicatorOffset}px, 0, 0)`
|
||||
: `translate3d(0, ${indicatorOffset}px, 0)`}
|
||||
></span>
|
||||
{/if}
|
||||
</TabsPrimitive.List>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"gap-2 rounded-full border border-transparent! px-3 py-1 text-sm font-medium group-data-vertical/tabs:rounded-2xl group-data-vertical/tabs:px-3 group-data-vertical/tabs:py-1.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
|
||||
'data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground',
|
||||
'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { page } from '$app/state';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(''),
|
||||
useHash = false,
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.RootProps = $props();
|
||||
}: TabsPrimitive.RootProps & {
|
||||
useHash?: boolean;
|
||||
} = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (useHash && page.url.hash) {
|
||||
value = page.url.hash.substring(1);
|
||||
}
|
||||
});
|
||||
|
||||
function onTabChange(newValue: string) {
|
||||
if (useHash && page.url.hash !== newValue) {
|
||||
window.location.hash = newValue;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
onValueChange={onTabChange}
|
||||
data-slot="tabs"
|
||||
class={cn('gap-2 group/tabs flex data-[orientation=horizontal]:flex-col', className)}
|
||||
{...restProps}
|
||||
|
||||
Reference in New Issue
Block a user