style: auto-format codebase with Prettier

Applied formatting to 1487+ files using pnpm format:write
  - TypeScript/JavaScript files
  - Svelte components
  - Astro pages
  - JSON configs
  - Markdown docs

  13 files still need manual review (Astro JSX comments)
This commit is contained in:
Wuesteon 2025-11-27 18:33:16 +01:00
parent 0241f5554c
commit d36b321d9d
3952 changed files with 661498 additions and 739751 deletions

View file

@ -11,12 +11,7 @@
children?: Snippet;
}
let {
variant = 'default',
size = 'md',
class: className = '',
children
}: Props = $props();
let { variant = 'default', size = 'md', class: className = '', children }: Props = $props();
const variantClasses: Record<BadgeVariant, string> = {
default: 'bg-menu text-theme border-theme',
@ -24,12 +19,12 @@
success: 'bg-green-500/20 text-green-600 dark:text-green-400 border-green-500/30',
warning: 'bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 border-yellow-500/30',
danger: 'bg-red-500/20 text-red-600 dark:text-red-400 border-red-500/30',
info: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30'
info: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-500/30',
};
const sizeClasses: Record<BadgeSize, string> = {
sm: 'px-1.5 py-0.5 text-xs',
md: 'px-2 py-1 text-sm'
md: 'px-2 py-1 text-sm',
};
const classes = $derived(

View file

@ -23,7 +23,7 @@
class: className = '',
onclick,
type = 'button',
children
children,
}: Props = $props();
const variantClasses: Record<ButtonVariant, string> = {
@ -32,14 +32,14 @@
ghost: 'bg-transparent text-theme hover:bg-menu-hover border-transparent',
danger: 'bg-red-600 text-white hover:bg-red-700 border-transparent',
outline: 'bg-transparent text-primary border-primary hover:bg-primary/10',
success: 'bg-green-600 text-white hover:bg-green-700 border-transparent'
success: 'bg-green-600 text-white hover:bg-green-700 border-transparent',
};
const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
xl: 'px-8 py-4 text-xl'
xl: 'px-8 py-4 text-xl',
};
const classes = $derived(
@ -47,16 +47,15 @@
);
</script>
<button
{type}
class={classes}
disabled={disabled || loading}
{onclick}
>
<button {type} class={classes} disabled={disabled || loading} {onclick}>
{#if loading}
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
{/if}
{@render children?.()}

View file

@ -34,7 +34,7 @@
onclick,
header,
footer,
children
children,
}: Props = $props();
// Determine if card should be interactive
@ -43,7 +43,9 @@
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div
class="card card--{variant} card--padding-{padding} {isInteractive ? 'card--interactive' : ''} {fullWidth ? 'card--full-width' : ''} {className}"
class="card card--{variant} card--padding-{padding} {isInteractive
? 'card--interactive'
: ''} {fullWidth ? 'card--full-width' : ''} {className}"
{onclick}
role={isInteractive ? 'button' : undefined}
tabindex={isInteractive ? 0 : undefined}
@ -82,7 +84,9 @@
.card--elevated {
background-color: hsl(var(--color-surface-elevated));
border: 1px solid hsl(var(--color-border));
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
box-shadow:
0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px -1px rgba(0, 0, 0, 0.1);
}
.card--outlined {
@ -130,7 +134,9 @@
}
.card--elevated.card--interactive:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -2px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}

View file

@ -28,20 +28,20 @@
'body-secondary': 'text-base text-theme-secondary leading-relaxed',
small: 'text-sm text-theme',
large: 'text-lg text-theme leading-relaxed',
muted: 'text-sm text-theme-muted'
muted: 'text-sm text-theme-muted',
};
const alignClasses: Record<TextAlign, string> = {
left: 'text-left',
center: 'text-center',
right: 'text-right'
right: 'text-right',
};
const weightClasses: Record<TextWeight, string> = {
normal: 'font-normal',
medium: 'font-medium',
semibold: 'font-semibold',
bold: 'font-bold'
bold: 'font-bold',
};
const classes = $derived(

View file

@ -28,7 +28,14 @@ export { Modal, ConfirmationModal, FormModal, AppSlider } from './organisms';
export type { AppItem } from './organisms';
// Navigation
export { NavLink, Navbar, Sidebar, SidebarSection, PillNavigation, PillDropdown } from './navigation';
export {
NavLink,
Navbar,
Sidebar,
SidebarSection,
PillNavigation,
PillDropdown,
} from './navigation';
export type {
NavItem,
NavbarProps,
@ -37,5 +44,5 @@ export type {
KeyboardShortcut,
PillNavItem,
PillDropdownItem,
PillNavigationProps
PillNavigationProps,
} from './navigation';

View file

@ -23,7 +23,7 @@
description,
disabled = false,
indeterminate = false,
class: className = ''
class: className = '',
}: Props = $props();
let inputElement: HTMLInputElement | null = $state(null);
@ -53,11 +53,23 @@
/>
<div class="checkbox-box">
{#if indeterminate}
<svg class="checkbox-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<svg
class="checkbox-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
>
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
{:else if checked}
<svg class="checkbox-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<svg
class="checkbox-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
>
<polyline points="20 6 9 17 4 12" />
</svg>
{/if}

View file

@ -67,14 +67,14 @@
metadata,
actions,
badge,
class: className = ''
class: className = '',
}: Props = $props();
const variantClasses: Record<CardVariant, string> = {
default: 'bg-menu border border-theme',
elevated: 'bg-menu border border-theme shadow-md',
outlined: 'bg-transparent border-2 border-theme',
ghost: 'bg-transparent border-transparent hover:bg-menu-hover'
ghost: 'bg-transparent border-transparent hover:bg-menu-hover',
};
const isClickable = $derived(interactive || !!onclick);
@ -84,7 +84,7 @@
class="data-card rounded-xl p-4 transition-colors {variantClasses[variant]} {isClickable
? 'cursor-pointer hover:bg-menu-hover'
: ''} {className}"
onclick={onclick}
{onclick}
onkeydown={(e) => {
if (isClickable && onclick && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
@ -139,7 +139,10 @@
{#if actions}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="data-card__actions flex-shrink-0 flex items-center gap-1" onclick={(e) => e.stopPropagation()}>
<div
class="data-card__actions flex-shrink-0 flex items-center gap-1"
onclick={(e) => e.stopPropagation()}
>
{@render actions()}
</div>
{/if}

View file

@ -30,7 +30,7 @@
autocomplete,
class: className = '',
id = `input-${Math.random().toString(36).slice(2, 9)}`,
name
name,
}: Props = $props();
function handleInput(e: Event) {
@ -70,7 +70,7 @@
? 'border-red-500 focus:ring-red-500/50'
: 'border-theme'}"
/>
{#if error}
<p class="text-sm text-red-500">{error}</p>
{/if}

View file

@ -59,7 +59,7 @@
expanded = $bindable(false),
collapsible = true,
compact = false,
class: className = ''
class: className = '',
}: Props = $props();
// Group shortcuts by category
@ -116,7 +116,12 @@
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
{/if}
</button>
@ -162,14 +167,9 @@
<button
type="button"
class="w-full flex items-center justify-center p-2 hover:bg-menu-hover rounded-lg transition-colors group relative"
title={title}
{title}
>
<svg
class="w-5 h-5 text-theme-secondary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<svg class="w-5 h-5 text-theme-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"

View file

@ -60,14 +60,14 @@
loading = false,
disabled = false,
align = 'end',
class: className = ''
class: className = '',
}: Props = $props();
const alignClasses: Record<string, string> = {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end',
between: 'justify-between'
between: 'justify-between',
};
</script>
@ -78,7 +78,7 @@
</Button>
{/if}
{#if confirmLabel && onConfirm}
<Button variant={confirmVariant} onclick={onConfirm} {loading} disabled={disabled}>
<Button variant={confirmVariant} onclick={onConfirm} {loading} {disabled}>
{confirmLabel}
</Button>
{/if}

View file

@ -69,22 +69,22 @@
breadcrumb,
actions,
tabs,
class: className = ''
class: className = '',
}: Props = $props();
const sizeClasses: Record<HeaderSize, { container: string; title: string }> = {
sm: {
container: 'py-3',
title: 'text-lg'
title: 'text-lg',
},
md: {
container: 'py-4',
title: 'text-xl'
title: 'text-xl',
},
lg: {
container: 'py-6',
title: 'text-2xl'
}
title: 'text-2xl',
},
};
</script>

View file

@ -34,7 +34,7 @@
disabled = false,
required = false,
class: className = '',
id = `select-${Math.random().toString(36).slice(2, 9)}`
id = `select-${Math.random().toString(36).slice(2, 9)}`,
}: Props = $props();
function handleChange(e: Event) {

View file

@ -44,7 +44,7 @@
required = false,
autoResize = false,
class: className = '',
id = `textarea-${Math.random().toString(36).slice(2, 9)}`
id = `textarea-${Math.random().toString(36).slice(2, 9)}`,
}: Props = $props();
let textareaElement: HTMLTextAreaElement | null = $state(null);
@ -90,7 +90,9 @@
{required}
oninput={handleInput}
onchange={handleChange}
class="textarea-input {error || isOverLimit ? 'textarea-input--error' : ''} {autoResize ? 'textarea-input--auto-resize' : ''}"
class="textarea-input {error || isOverLimit ? 'textarea-input--error' : ''} {autoResize
? 'textarea-input--auto-resize'
: ''}"
></textarea>
<div class="textarea-footer">

View file

@ -16,7 +16,7 @@
const sizeClasses = {
sm: { track: 'h-6 w-10', thumb: 'h-4 w-4 top-1 left-1', translate: 'translate-x-4' },
md: { track: 'h-8 w-14', thumb: 'h-6 w-6 top-1 left-1', translate: 'translate-x-6' }
md: { track: 'h-8 w-14', thumb: 'h-6 w-6 top-1 left-1', translate: 'translate-x-6' },
};
</script>
@ -31,7 +31,8 @@
{disabled}
>
<span
class="absolute {sizeClasses[size].thumb} rounded-full bg-white shadow-md transition-transform {isOn
class="absolute {sizeClasses[size]
.thumb} rounded-full bg-white shadow-md transition-transform {isOn
? sizeClasses[size].translate
: 'translate-x-0'}"
></span>

View file

@ -63,18 +63,20 @@
onSecondaryAction,
variant = 'default',
icon,
class: className = ''
class: className = '',
}: Props = $props();
const variantClasses: Record<EmptyStateVariant, string> = {
default: 'py-12 px-6',
compact: 'py-6 px-4',
centered: 'py-16 px-8'
centered: 'py-16 px-8',
};
</script>
<div
class="empty-state flex flex-col items-center justify-center text-center {variantClasses[variant]} {className}"
class="empty-state flex flex-col items-center justify-center text-center {variantClasses[
variant
]} {className}"
>
<!-- Icon -->
{#if icon}

View file

@ -30,7 +30,7 @@
height = '20px',
borderRadius = '4px',
circle = false,
class: className = ''
class: className = '',
}: Props = $props();
const computedRadius = $derived(circle ? '50%' : borderRadius);

View file

@ -31,15 +31,12 @@
lineHeight = '16px',
gap = '8px',
lastLineWidth = '70%',
class: className = ''
class: className = '',
}: Props = $props();
</script>
<div class="skeleton-text {className}" style="display: flex; flex-direction: column; gap: {gap};">
{#each Array(lines) as _, i}
<SkeletonBox
width={i === lines - 1 ? lastLineWidth : '100%'}
height={lineHeight}
/>
<SkeletonBox width={i === lines - 1 ? lastLineWidth : '100%'} height={lineHeight} />
{/each}
</div>

View file

@ -93,9 +93,7 @@
}
// Calculate progress percentage for styling
const progressPercent = $derived(
audioDuration > 0 ? (currentTime / audioDuration) * 100 : 0
);
const progressPercent = $derived(audioDuration > 0 ? (currentTime / audioDuration) * 100 : 0);
</script>
<div class="rounded-2xl border border-theme bg-content p-4">
@ -133,7 +131,12 @@
{@render skipBackIcon()}
{:else}
<svg class="h-6 w-6 text-theme" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0019 16V8a1 1 0 00-1.6-.8l-5.333 4zM4.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0011 16V8a1 1 0 00-1.6-.8l-5.334 4z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0019 16V8a1 1 0 00-1.6-.8l-5.333 4zM4.066 11.2a1 1 0 000 1.6l5.334 4A1 1 0 0011 16V8a1 1 0 00-1.6-.8l-5.334 4z"
/>
</svg>
{/if}
</button>
@ -148,7 +151,8 @@
>
{#if isLoading}
<svg class="h-6 w-6 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
@ -163,14 +167,12 @@
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
</svg>
{/if}
{:else if playIcon}
{@render playIcon()}
{:else}
{#if playIcon}
{@render playIcon()}
{:else}
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
{/if}
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
{/if}
</button>
@ -186,7 +188,12 @@
{@render skipForwardIcon()}
{:else}
<svg class="h-6 w-6 text-theme" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.933 12.8a1 1 0 000-1.6L6.6 7.2A1 1 0 005 8v8a1 1 0 001.6.8l5.333-4zM19.933 12.8a1 1 0 000-1.6l-5.333-4A1 1 0 0013 8v8a1 1 0 001.6.8l5.333-4z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M11.933 12.8a1 1 0 000-1.6L6.6 7.2A1 1 0 005 8v8a1 1 0 001.6.8l5.333-4zM19.933 12.8a1 1 0 000-1.6l-5.333-4A1 1 0 0013 8v8a1 1 0 001.6.8l5.333-4z"
/>
</svg>
{/if}
</button>

View file

@ -28,13 +28,7 @@
onClick?: () => void;
}
let {
tag,
removable = false,
clickable = false,
onRemove,
onClick
}: Props = $props();
let { tag, removable = false, clickable = false, onRemove, onClick }: Props = $props();
// Get tag color from either style.color (new format) or color (old format)
const tagColor = $derived(tag.style?.color || tag.color || '#3b82f6');
@ -84,7 +78,12 @@
aria-label="Remove tag"
>
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/if}

View file

@ -6,7 +6,7 @@
active = false,
variant = 'default',
minimized = false,
class: className = ''
class: className = '',
}: NavLinkProps = $props();
let showTooltip = $state(false);
@ -24,7 +24,9 @@
<a
href={item.href}
class="nav-link nav-link--{variant} {active ? 'nav-link--active' : ''} {minimized ? 'nav-link--minimized' : ''} {className}"
class="nav-link nav-link--{variant} {active ? 'nav-link--active' : ''} {minimized
? 'nav-link--minimized'
: ''} {className}"
class:nav-link--disabled={item.disabled}
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
@ -35,7 +37,9 @@
<span class="nav-link__icon">
{#if item.icon.startsWith('<svg') || item.icon.startsWith('M')}
<!-- SVG path or element -->
{@html item.icon.startsWith('M') ? `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="${item.icon}"/></svg>` : item.icon}
{@html item.icon.startsWith('M')
? `<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="${item.icon}"/></svg>`
: item.icon}
{:else}
<!-- Emoji or text icon -->
<span class="text-lg">{item.icon}</span>

View file

@ -33,7 +33,7 @@
userEmail = '',
onSignOut,
signOutLabel = 'Sign Out',
class: className = ''
class: className = '',
}: Props = $props();
let mobileMenuOpen = $state(false);
@ -91,11 +91,21 @@
>
{#if mobileMenuOpen}
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
{:else}
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
{/if}
</button>

View file

@ -10,14 +10,7 @@
onToggle?: (open: boolean) => void;
}
let {
items,
direction = 'down',
label,
icon,
isOpen = false,
onToggle
}: Props = $props();
let { items, direction = 'down', label, icon, isOpen = false, onToggle }: Props = $props();
let internalOpen = $state(false);
let triggerButton: HTMLButtonElement;
@ -31,12 +24,12 @@
if (direction === 'down') {
dropdownPosition = {
top: rect.bottom + 8,
left: rect.left
left: rect.left,
};
} else {
dropdownPosition = {
top: rect.top - 8,
left: rect.left
left: rect.left,
};
}
}
@ -62,10 +55,12 @@
}
const iconPaths: Record<string, string> = {
language: 'M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129',
language:
'M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129',
check: 'M5 13l4 4L19 7',
chevronDown: 'M19 9l-7 7-7-7',
globe: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9'
globe:
'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9',
};
function getIcon(iconName: string) {
@ -75,24 +70,10 @@
<div class="pill-dropdown">
<!-- Trigger Button -->
<button
bind:this={triggerButton}
onclick={toggle}
class="pill glass-pill trigger-button"
>
<button bind:this={triggerButton} onclick={toggle} class="pill glass-pill trigger-button">
{#if icon}
<svg
class="pill-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIcon(icon)}
/>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIcon(icon)} />
</svg>
{/if}
<span class="pill-label">{label}</span>
@ -114,10 +95,7 @@
{#if open}
<!-- Backdrop -->
<button
class="menu-backdrop"
onclick={close}
onkeydown={(e) => e.key === 'Escape' && close()}
<button class="menu-backdrop" onclick={close} onkeydown={(e) => e.key === 'Escape' && close()}
></button>
<!-- Dropdown items -->
@ -127,7 +105,7 @@
class:fan-down={direction === 'down'}
style="top: {dropdownPosition.top}px; left: {dropdownPosition.left}px;"
>
{#each items.filter(i => !i.disabled) as item, i (item.id)}
{#each items.filter((i) => !i.disabled) as item, i (item.id)}
<button
onclick={() => handleItemClick(item)}
class="pill glass-pill fan-pill"
@ -136,12 +114,7 @@
style="animation-delay: {i * 15}ms"
>
{#if item.icon}
<svg
class="pill-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -152,12 +125,7 @@
{/if}
<span class="pill-label">{item.label}</span>
{#if item.active}
<svg
class="check-icon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<svg class="check-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
@ -247,7 +215,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: #374151;
}
@ -261,7 +231,9 @@
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .glass-pill:hover {

View file

@ -57,7 +57,7 @@
currentLanguageLabel = 'Language',
showLanguageSwitcher = false,
showThemeToggle = true,
primaryColor
primaryColor,
}: Props = $props();
// Local state for uncontrolled mode
@ -65,8 +65,12 @@
let internalCollapsed = $state(false);
// Use external or internal state
const isSidebarMode = $derived(onModeChange !== undefined ? (externalSidebarMode ?? false) : internalSidebarMode);
const isCollapsed = $derived(onCollapsedChange !== undefined ? (externalCollapsed ?? false) : internalCollapsed);
const isSidebarMode = $derived(
onModeChange !== undefined ? (externalSidebarMode ?? false) : internalSidebarMode
);
const isCollapsed = $derived(
onCollapsedChange !== undefined ? (externalCollapsed ?? false) : internalCollapsed
);
function toggleSidebarMode() {
const newValue = !isSidebarMode;
@ -102,25 +106,33 @@
mic: 'M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z',
archive: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4',
upload: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12',
music: 'M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3',
music:
'M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3',
tag: 'M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z',
document: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
chart: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
settings: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
document:
'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
chart:
'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
settings:
'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
settingsInner: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z',
home: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
users: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z',
users:
'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z',
user: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z',
building: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
creditCard: 'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z',
building:
'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
creditCard:
'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z',
search: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
moon: 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z',
sun: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z',
logout: 'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1',
logout:
'M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1',
chevronDown: 'M19 9l-7 7-7-7',
chevronUp: 'M5 15l7-7 7 7',
chevronLeft: 'M15 19l-7-7 7-7',
menu: 'M4 6h16M4 12h16M4 18h16'
menu: 'M4 6h16M4 12h16M4 18h16',
};
function getIconPath(name: string): string {
@ -129,153 +141,190 @@
</script>
{#if !isCollapsed}
<nav class="pill-nav" class:sidebar-mode={isSidebarMode} style={primaryColor ? `--pill-primary-color: ${primaryColor}` : ''}>
<div class="pill-nav-container" class:sidebar-container={isSidebarMode}>
<!-- Control Button (left position in horizontal mode) -->
{#if !isSidebarMode}
<div class="pill glass-pill segmented-control">
<button
onclick={toggleSidebarMode}
class="segment-btn"
title="Switch to sidebar navigation"
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('chevronDown')} />
</svg>
</button>
<div class="segment-divider"></div>
<button
onclick={collapseNav}
class="segment-btn"
title="Collapse navigation"
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('chevronLeft')} />
</svg>
</button>
</div>
{/if}
<!-- Logo pill -->
<a href={homeRoute} class="pill glass-pill logo-pill">
{#if logo}
{@render logo()}
{:else}
<span class="pill-label font-bold">{appName}</span>
<nav
class="pill-nav"
class:sidebar-mode={isSidebarMode}
style={primaryColor ? `--pill-primary-color: ${primaryColor}` : ''}
>
<div class="pill-nav-container" class:sidebar-container={isSidebarMode}>
<!-- Control Button (left position in horizontal mode) -->
{#if !isSidebarMode}
<div class="pill glass-pill segmented-control">
<button
onclick={toggleSidebarMode}
class="segment-btn"
title="Switch to sidebar navigation"
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('chevronDown')}
/>
</svg>
</button>
<div class="segment-divider"></div>
<button onclick={collapseNav} class="segment-btn" title="Collapse navigation">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('chevronLeft')}
/>
</svg>
</button>
</div>
{/if}
</a>
<!-- Navigation Items -->
{#each items as item}
<a
href={item.href}
class="pill glass-pill"
class:active={isActive(item.href)}
>
{#if item.icon}
{#if item.icon === 'mana'}
<svg class="pill-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z" />
<!-- Logo pill -->
<a href={homeRoute} class="pill glass-pill logo-pill">
{#if logo}
{@render logo()}
{:else}
<span class="pill-label font-bold">{appName}</span>
{/if}
</a>
<!-- Navigation Items -->
{#each items as item}
<a href={item.href} class="pill glass-pill" class:active={isActive(item.href)}>
{#if item.icon}
{#if item.icon === 'mana'}
<svg class="pill-icon" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12.3047 1C12.3392 1.04573 19.608 10.6706 19.6084 14.6953C19.6084 18.7293 16.3386 21.9998 12.3047 22C8.27061 22 5 18.7294 5 14.6953C5.00041 10.661 12.3047 1 12.3047 1ZM12.3047 7.3916C12.2811 7.42276 8.65234 12.2288 8.65234 14.2393C8.65241 16.2562 10.2877 17.8916 12.3047 17.8916C14.3217 17.8916 15.957 16.2562 15.957 14.2393C15.957 12.2301 12.3331 7.42917 12.3047 7.3916Z"
/>
</svg>
{:else if item.icon === 'settings'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('settings')}
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('settingsInner')}
/>
</svg>
{:else if item.iconSvg}
{@html item.iconSvg}
{:else}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath(item.icon)}
/>
</svg>
{/if}
{/if}
<span class="pill-label">{item.label}</span>
</a>
{/each}
<!-- Language Switcher -->
{#if showLanguageSwitcher && languageItems.length > 0}
<PillDropdown
items={languageItems}
direction="down"
label={currentLanguageLabel}
icon="globe"
/>
{/if}
<!-- Theme Toggle -->
{#if showThemeToggle && onToggleTheme}
<button
onclick={onToggleTheme}
class="pill glass-pill"
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{#if !isDark}
<svg class="pill-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('moon')}
/>
</svg>
{:else if item.icon === 'settings'}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('settings')} />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('settingsInner')} />
</svg>
{:else if item.iconSvg}
{@html item.iconSvg}
{:else}
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath(item.icon)} />
<svg class="pill-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('sun')}
/>
</svg>
{/if}
{/if}
<span class="pill-label">{item.label}</span>
</a>
{/each}
<!-- Language Switcher -->
{#if showLanguageSwitcher && languageItems.length > 0}
<PillDropdown
items={languageItems}
direction="down"
label={currentLanguageLabel}
icon="globe"
/>
{/if}
<!-- Theme Toggle -->
{#if showThemeToggle && onToggleTheme}
<button
onclick={onToggleTheme}
class="pill glass-pill"
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{#if !isDark}
<svg class="pill-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('moon')} />
</svg>
{:else}
<svg class="pill-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('sun')} />
</svg>
{/if}
<span class="pill-label">{isDark ? 'Light' : 'Dark'}</span>
</button>
{/if}
<!-- Logout -->
{#if onLogout}
<button
onclick={onLogout}
class="pill glass-pill logout-pill"
title="Logout"
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('logout')} />
</svg>
<span class="pill-label">Logout</span>
</button>
{/if}
<!-- Control Button (bottom position in sidebar mode) -->
{#if isSidebarMode}
<div class="sidebar-spacer"></div>
<div class="pill glass-pill segmented-control sidebar-segmented">
<button
onclick={toggleSidebarMode}
class="segment-btn"
title="Switch to top navigation"
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('chevronUp')} />
</svg>
<span class="pill-label">{isDark ? 'Light' : 'Dark'}</span>
</button>
<div class="segment-divider"></div>
<button
onclick={collapseNav}
class="segment-btn"
title="Collapse navigation"
>
{/if}
<!-- Logout -->
{#if onLogout}
<button onclick={onLogout} class="pill glass-pill logout-pill" title="Logout">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('chevronLeft')} />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('logout')}
/>
</svg>
<span class="pill-label">Logout</span>
</button>
</div>
{/if}
</div>
</nav>
{/if}
<!-- Control Button (bottom position in sidebar mode) -->
{#if isSidebarMode}
<div class="sidebar-spacer"></div>
<div class="pill glass-pill segmented-control sidebar-segmented">
<button onclick={toggleSidebarMode} class="segment-btn" title="Switch to top navigation">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('chevronUp')}
/>
</svg>
</button>
<div class="segment-divider"></div>
<button onclick={collapseNav} class="segment-btn" title="Collapse navigation">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('chevronLeft')}
/>
</svg>
</button>
</div>
{/if}
</div>
</nav>
{/if}
<!-- FAB for collapsed state -->
{#if isCollapsed}
<button
onclick={expandNav}
class="nav-fab glass-pill"
title="Expand navigation"
>
<button onclick={expandNav} class="nav-fab glass-pill" title="Expand navigation">
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={getIconPath('menu')} />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={getIconPath('menu')}
/>
</svg>
</button>
{/if}
@ -328,7 +377,9 @@
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: #374151;
}
@ -342,7 +393,9 @@
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .glass-pill:hover {
@ -353,13 +406,21 @@
/* Active state - uses CSS custom property for theming */
.pill.active {
background: var(--pill-primary-color, var(--color-primary-500, rgba(248, 214, 43, 0.9)));
background: color-mix(in srgb, var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 20%, white 80%);
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 20%,
white 80%
);
border-color: var(--pill-primary-color, var(--color-primary-500, rgba(248, 214, 43, 0.5)));
color: #1a1a1a;
}
:global(.dark) .pill.active {
background: color-mix(in srgb, var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 30%, transparent 70%);
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 30%,
transparent 70%
);
border-color: var(--pill-primary-color, var(--color-primary-500, rgba(248, 214, 43, 0.4)));
color: var(--pill-primary-color, var(--color-primary-500, #f8d62b));
}
@ -448,13 +509,29 @@
/* Keep active state visible */
.sidebar-container .pill.active {
background: color-mix(in srgb, var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 20%, transparent 80%);
border-color: color-mix(in srgb, var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 30%, transparent 70%);
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 20%,
transparent 80%
);
border-color: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 30%,
transparent 70%
);
}
:global(.dark) .sidebar-container .pill.active {
background: color-mix(in srgb, var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 15%, transparent 85%);
border-color: color-mix(in srgb, var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 25%, transparent 75%);
background: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 15%,
transparent 85%
);
border-color: color-mix(
in srgb,
var(--pill-primary-color, var(--color-primary-500, #f8d62b)) 25%,
transparent 75%
);
}
/* Logo pill in sidebar - same as other pills (transparent) */

View file

@ -63,7 +63,7 @@
minimizeLabel = 'Minimize',
expandLabel = 'Expand',
class: className = '',
footer
footer,
}: Props = $props();
function isActive(href: string): boolean {
@ -89,12 +89,7 @@
<!-- Navigation -->
<nav class="sidebar__nav">
{#each items as item}
<NavLink
{item}
active={isActive(item.href)}
variant="sidebar"
{minimized}
/>
<NavLink {item} active={isActive(item.href)} variant="sidebar" {minimized} />
{/each}
</nav>
@ -114,12 +109,22 @@
{#if isDark}
<!-- Sun icon -->
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{:else}
<!-- Moon icon -->
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
{/if}
{#if !minimized}
@ -141,7 +146,12 @@
title={signOutLabel}
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
{#if !minimized}
<span>{signOutLabel}</span>
@ -159,12 +169,22 @@
{#if minimized}
<!-- Menu icon (expand) -->
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
{:else}
<!-- Chevron left (minimize) -->
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
<span>{minimizeLabel}</span>
{/if}

View file

@ -52,7 +52,7 @@
collapsible = false,
expanded = $bindable(true),
divider = false,
class: className = ''
class: className = '',
}: Props = $props();
function isActive(item: NavItem): boolean {
@ -88,7 +88,12 @@
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{:else}

View file

@ -12,5 +12,5 @@ export type {
KeyboardShortcut,
PillNavItem,
PillDropdownItem,
PillNavigationProps
PillNavigationProps,
} from './types';

View file

@ -24,11 +24,11 @@
published: 'Live',
beta: 'Beta',
development: 'In Development',
planning: 'Planned'
planning: 'Planned',
},
comingSoonLabel = 'Coming Soon',
openAppLabel = 'Open App',
onAppClick
onAppClick,
}: Props = $props();
let selectedApp = $state<number | null>(null);
@ -41,7 +41,7 @@
published: '#4CAF50',
beta: '#FFD700',
development: '#FF9800',
planning: '#F44336'
planning: '#F44336',
};
return colors[status];
}
@ -91,7 +91,7 @@
const scrollPosition = appIndex * cardWidth;
modalScrollContainer?.scrollTo({
left: scrollPosition,
behavior: 'smooth'
behavior: 'smooth',
});
}, 50);
}
@ -107,21 +107,32 @@
</h3>
<div class="relative">
<div class="flex gap-4 justify-center overflow-x-auto pb-6 scrollbar-hide snap-x snap-mandatory scroll-smooth px-4 py-4" style="perspective: 1000px;">
<div
class="flex gap-4 justify-center overflow-x-auto pb-6 scrollbar-hide snap-x snap-mandatory scroll-smooth px-4 py-4"
style="perspective: 1000px;"
>
{#each apps as app, index}
<button
class="group relative flex-shrink-0 rounded-xl p-5 cursor-pointer snap-center transition-transform hover:scale-105"
style="width: 160px; background-color: {isDark ? 'rgba(255, 255, 255, 0.08)' : 'rgba(255, 255, 255, 0.7)'}; backdrop-filter: blur(10px); border: 1px solid {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'};"
onmouseenter={() => hoveredApp = index}
onmouseleave={() => hoveredApp = null}
style="width: 160px; background-color: {isDark
? 'rgba(255, 255, 255, 0.08)'
: 'rgba(255, 255, 255, 0.7)'}; backdrop-filter: blur(10px); border: 1px solid {isDark
? 'rgba(255, 255, 255, 0.1)'
: 'rgba(0, 0, 0, 0.1)'};"
onmouseenter={() => (hoveredApp = index)}
onmouseleave={() => (hoveredApp = null)}
onclick={() => openModal(index)}
>
<div
class="absolute top-3 right-3 w-3 h-3 rounded-full status-indicator"
style="background-color: {getStatusColor(app.status)}; box-shadow: 0 0 8px {getStatusColor(app.status)};"
style="background-color: {getStatusColor(
app.status
)}; box-shadow: 0 0 8px {getStatusColor(app.status)};"
></div>
<div class="mb-2 flex h-20 w-20 mx-auto items-center justify-center rounded-xl transition-transform group-hover:scale-110">
<div
class="mb-2 flex h-20 w-20 mx-auto items-center justify-center rounded-xl transition-transform group-hover:scale-110"
>
{#if app.icon}
<img src={app.icon} alt={app.name} class="w-16 h-16 object-contain" />
{:else}
@ -162,7 +173,12 @@
aria-label="Close modal"
>
<svg class="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
@ -174,22 +190,42 @@
{#each apps as app, index}
<div
class="flex-shrink-0 rounded-3xl p-8 snap-center shadow-2xl relative"
style="min-width: 360px; max-width: 360px; background-color: {hoveredApp === index ? (isDark ? '#2A2A2A' : '#F5F5F5') : (isDark ? '#1E1E1E' : '#ffffff')}; border: 3px solid {app.color}40; transform: perspective(1000px) rotateX({cardRotations[index]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY || 0}deg); transform-style: preserve-3d; transition: transform 0.1s ease-out, background-color 0.2s ease-out;"
onclick={(e) => { e.stopPropagation(); selectedApp = index; }}
onmouseenter={() => hoveredApp = index}
style="min-width: 360px; max-width: 360px; background-color: {hoveredApp === index
? isDark
? '#2A2A2A'
: '#F5F5F5'
: isDark
? '#1E1E1E'
: '#ffffff'}; border: 3px solid {app.color}40; transform: perspective(1000px) rotateX({cardRotations[
index
]?.rotateX || 0}deg) rotateY({cardRotations[index]?.rotateY ||
0}deg); transform-style: preserve-3d; transition: transform 0.1s ease-out, background-color 0.2s ease-out;"
onclick={(e) => {
e.stopPropagation();
selectedApp = index;
}}
onmouseenter={() => (hoveredApp = index)}
onmousemove={(e) => handleCardMouseMove(e, index, e.currentTarget)}
onmouseleave={() => { handleCardMouseLeave(index); hoveredApp = null; }}
onmouseleave={() => {
handleCardMouseLeave(index);
hoveredApp = null;
}}
onkeydown={() => {}}
role="button"
tabindex="0"
>
<div class="absolute top-4 right-4 flex items-center gap-2">
<span class="text-xs font-medium" style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};">
<span
class="text-xs font-medium"
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
>
{getStatusLabel(app.status)}
</span>
<div
class="w-4 h-4 rounded-full status-indicator"
style="background-color: {getStatusColor(app.status)}; box-shadow: 0 0 12px {getStatusColor(app.status)};"
style="background-color: {getStatusColor(
app.status
)}; box-shadow: 0 0 12px {getStatusColor(app.status)};"
></div>
</div>
@ -204,7 +240,10 @@
</div>
{/if}
<h3 class="text-2xl font-bold mb-2 text-center" style="color: {isDark ? '#ffffff' : '#000000'};">
<h3
class="text-2xl font-bold mb-2 text-center"
style="color: {isDark ? '#ffffff' : '#000000'};"
>
{app.name}
</h3>
@ -212,7 +251,10 @@
{app.description}
</p>
<p class="text-sm leading-relaxed mb-6 text-center" style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};">
<p
class="text-sm leading-relaxed mb-6 text-center"
style="color: {isDark ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'};"
>
{app.longDescription}
</p>
@ -220,7 +262,11 @@
{#if app.comingSoon}
<div
class="inline-block rounded-full px-5 py-2.5 text-sm font-medium"
style="background-color: {isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}; color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
style="background-color: {isDark
? 'rgba(255, 255, 255, 0.1)'
: 'rgba(0, 0, 0, 0.1)'}; color: {isDark
? 'rgba(255, 255, 255, 0.5)'
: 'rgba(0, 0, 0, 0.5)'};"
>
{comingSoonLabel}
</div>
@ -228,7 +274,10 @@
<button
class="rounded-xl px-8 py-3 text-sm font-semibold transition-all hover:opacity-80 border-2 text-white"
style="background-color: {app.color}60; border-color: {app.color};"
onclick={(e) => { e.stopPropagation(); handleAppAction(app, index); }}
onclick={(e) => {
e.stopPropagation();
handleAppAction(app, index);
}}
>
{openAppLabel}
</button>
@ -256,7 +305,8 @@
}
@keyframes pulse {
0%, 100% {
0%,
100% {
opacity: 1;
}
50% {

View file

@ -67,7 +67,7 @@
message,
confirmLabel = 'Confirm',
cancelLabel = 'Cancel',
loading = false
loading = false,
}: Props = $props();
const variantConfig: Record<
@ -77,18 +77,18 @@
danger: {
iconName: 'alert-triangle',
iconColor: 'text-red-500',
buttonVariant: 'danger'
buttonVariant: 'danger',
},
warning: {
iconName: 'alert-circle',
iconColor: 'text-yellow-500',
buttonVariant: 'primary'
buttonVariant: 'primary',
},
info: {
iconName: 'info',
iconColor: 'text-blue-500',
buttonVariant: 'primary'
}
buttonVariant: 'primary',
},
};
const config = $derived(variantConfig[variant]);

View file

@ -68,7 +68,7 @@
loading = false,
error = null,
maxWidth = 'md',
submitDisabled = false
submitDisabled = false,
}: Props = $props();
async function handleSubmit(e: Event) {
@ -87,7 +87,9 @@
<form onsubmit={handleSubmit} onkeydown={handleKeydown} class="space-y-4">
<!-- Error message -->
{#if error}
<div class="rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3">
<div
class="rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3"
>
<Text variant="small" class="text-red-600 dark:text-red-400">
{error}
</Text>

View file

@ -14,7 +14,16 @@
showHeader?: boolean;
}
let { visible, onClose, title, icon, children, footer, maxWidth = 'lg', showHeader = true }: Props = $props();
let {
visible,
onClose,
title,
icon,
children,
footer,
maxWidth = 'lg',
showHeader = true,
}: Props = $props();
const maxWidthClasses = {
sm: 'max-w-sm',
@ -22,7 +31,7 @@
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
'3xl': 'max-w-3xl'
'3xl': 'max-w-3xl',
};
function handleBackdropClick(e: MouseEvent) {
@ -54,7 +63,9 @@
<!-- Modal Content -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="relative flex max-h-[90vh] w-full {maxWidthClasses[maxWidth]} flex-col rounded-xl border border-theme bg-menu shadow-xl"
class="relative flex max-h-[90vh] w-full {maxWidthClasses[
maxWidth
]} flex-col rounded-xl border border-theme bg-menu shadow-xl"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
>