managarten/packages/shared-ui/src/navigation/PillTabGroup.svelte
Till JS 637333051b feat(pill-nav): collapse user pills into account dropdown + solid pill backgrounds
Profile/Settings/Spiral/Credits move out of the standalone nav pills and
into the user-menu dropdown so the bottom bar stays compact. The dropdown
now also renders for guests (login users) — auth-only items (Profil,
Mana, Feedback, Logout) get filtered out, and a primary-styled "Anmelden"
entry replaces Logout. Themes is dropped from the dropdown since it
already has its own theme-variant pill.

New PillNavigation props: creditsHref, guestMenuLabel. New PillDropdown
icon paths: creditCard, spiral. New PillDropdownItem flag: primary
(prominent CTA styling), used for the guest Anmelden item.

All .glass-pill classes across PillNavigation, PillDropdown, PillTabGroup,
PillTagSelector, PillViewSwitcher, PillTimeRangeSelector, PillToolbar,
AppDrawer and ExpandableToolbar move from rgba+backdrop-blur to solid
theme tokens (hsl(var(--color-card)) / --color-border / --color-foreground)
so pills are fully opaque and follow the active theme variant instead of
having a frosted look that varied by background.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:40:19 +02:00

209 lines
4.3 KiB
Svelte

<script lang="ts">
import type { PillTabOption } from './types';
import {
List,
Columns,
Tag,
Heart,
House,
Gear,
GridFour,
Clock,
Timer,
Target,
CalendarBlank,
Fire,
MagnifyingGlass,
CheckSquare,
Funnel,
} from '@mana/shared-icons';
// Map icon names to Phosphor components
const phosphorIcons: Record<string, any> = {
list: List,
columns: Columns,
kanban: Columns,
tag: Tag,
heart: Heart,
home: House,
settings: Gear,
grid: GridFour,
clock: Clock,
timer: Timer,
target: Target,
calendar: CalendarBlank,
fire: Fire,
search: MagnifyingGlass,
'check-square': CheckSquare,
filter: Funnel,
};
interface Props {
/** Tab options to display */
options: PillTabOption[];
/** Currently selected tab id */
value: string;
/** Called when selection changes */
onChange: (id: string) => void;
/** Optional section label */
sectionLabel?: string;
/** Primary color for active state */
primaryColor?: string;
/** Called on right-click (context menu) - receives click coordinates */
onContextMenu?: (x: number, y: number) => void;
}
let { options, value, onChange, sectionLabel, primaryColor, onContextMenu }: Props = $props();
function handleContextMenu(event: MouseEvent) {
if (onContextMenu) {
event.preventDefault();
onContextMenu(event.clientX, event.clientY);
}
}
function handleClick(optionId: string, disabled?: boolean) {
if (!disabled) {
onChange(optionId);
}
}
</script>
<!-- svelte-ignore a11y_interactive_supports_focus -->
<div class="pill-tab-group" oncontextmenu={handleContextMenu} role="tablist">
<div
class="tab-container glass-pill"
style={primaryColor ? `--pill-primary-color: ${primaryColor}` : ''}
>
{#each options as option, index}
{#if index > 0}
<div class="tab-divider"></div>
{/if}
<button
onclick={() => handleClick(option.id, option.disabled)}
class="tab-btn"
class:active={value === option.id}
class:disabled={option.disabled}
title={option.title || option.label}
disabled={option.disabled}
>
{#if option.icon}
{#if option.iconSvg}
{@html option.iconSvg}
{:else if phosphorIcons[option.icon]}
{@const IconComponent = phosphorIcons[option.icon]}
<IconComponent size={18} class="tab-icon" />
{/if}
{/if}
{#if option.label}
<span class="tab-label">{option.label}</span>
{/if}
</button>
{/each}
</div>
</div>
<style>
.pill-tab-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.tab-container {
display: flex;
align-items: center;
padding: 0;
gap: 0;
border-radius: 9999px;
}
/* Solid theme-tokened pill (formerly the "glass" frosted pill). */
.glass-pill {
background: hsl(var(--color-card));
border: 1px solid hsl(var(--color-border));
box-shadow:
0 1px 2px hsl(0 0% 0% / 0.05),
0 2px 6px hsl(0 0% 0% / 0.04);
}
.tab-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
background: transparent;
border: none;
cursor: pointer;
color: #6b7280;
transition: all 0.2s;
flex: 1;
}
:global(.dark) .tab-btn {
color: #9ca3af;
}
.tab-btn:first-child {
border-radius: 9999px 0 0 9999px;
}
.tab-btn:last-child {
border-radius: 0 9999px 9999px 0;
}
.tab-btn:only-child {
border-radius: 9999px;
}
.tab-btn:hover:not(.disabled) {
background: rgba(0, 0, 0, 0.05);
color: #374151;
}
:global(.dark) .tab-btn:hover:not(.disabled) {
background: rgba(255, 255, 255, 0.1);
color: #f3f4f6;
}
.tab-btn.active {
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #3b82f6)) 20%,
white 80%
);
color: var(--pill-primary-color, var(--color-primary-500, #3b82f6));
}
:global(.dark) .tab-btn.active {
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #3b82f6)) 30%,
transparent 70%
);
color: var(--pill-primary-color, var(--color-primary-500, #3b82f6));
}
.tab-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.tab-divider {
width: 1px;
height: 1rem;
background: rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
:global(.dark) .tab-divider {
background: rgba(255, 255, 255, 0.15);
}
.tab-label {
font-size: 0.8125rem;
font-weight: 500;
white-space: nowrap;
}
</style>