mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 11:49:39 +02:00
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:
parent
0241f5554c
commit
d36b321d9d
3952 changed files with 661498 additions and 739751 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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?.()}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
height = '20px',
|
||||
borderRadius = '4px',
|
||||
circle = false,
|
||||
class: className = ''
|
||||
class: className = '',
|
||||
}: Props = $props();
|
||||
|
||||
const computedRadius = $derived(circle ? '50%' : borderRadius);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) */
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,5 @@ export type {
|
|||
KeyboardShortcut,
|
||||
PillNavItem,
|
||||
PillDropdownItem,
|
||||
PillNavigationProps
|
||||
PillNavigationProps,
|
||||
} from './types';
|
||||
|
|
|
|||
|
|
@ -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% {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue