use melt menu in sidebar

This commit is contained in:
Guilhem
2025-02-04 09:16:26 +00:00
parent b0e3fda414
commit 41e46238d3
7 changed files with 469 additions and 154 deletions
@@ -0,0 +1,58 @@
<script lang="ts">
import Portal from '$lib/components/Portal.svelte'
import { melt, createSync } from '@melt-ui/svelte'
import { twMerge } from 'tailwind-merge'
import ResolveOpen from '$lib/components/common/menu/ResolveOpen.svelte'
export let placement: any = 'right-start'
export let justifyEnd: boolean = false
export let lightMode: boolean = false
export let maxHeight: number = 900
export let disabled = false
export let createMenu: (any) => any
export let invisible: boolean = false
// Use the passed createMenu function
const menu = createMenu({
positioning: {
placement
}
})
//Melt
const {
elements: { trigger, menu: menuElement, item },
states
} = menu
let open = false
const sync = createSync(states)
$: sync.open(open, (v) => (open = Boolean(v)))
</script>
<div class="w-full h-8">
<ResolveOpen {open} on:open on:close />
<button class={twMerge('w-full', justifyEnd ? 'flex justify-end' : '')} {disabled}>
<slot name="trigger" trigger={$trigger} />
</button>
<Portal name="menu-v3">
<div class="z-[6000]" use:melt={$menuElement}>
<div
class={twMerge(
'border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto',
lightMode ? 'bg-surface-inverse' : 'bg-surface',
invisible ? 'opacity-0' : ''
)}
style="max-height: {maxHeight}px;"
>
<div class="my-1">
<slot item={$item} />
</div>
</div>
</div>
</Portal>
</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import { melt } from '@melt-ui/svelte'
export let createMenu: () => any
const menu = createMenu()
const {
elements: { trigger, item, menu: menuElement }
} = menu
</script>
<div class="hover:z-50 flex w-full h-8">
<slot name="trigger" trigger={$trigger} />
<div use:melt={$menuElement}>
<slot item={$item} />
</div>
</div>
@@ -0,0 +1,12 @@
<script lang="ts">
import { createMenubar, melt } from '@melt-ui/svelte'
const {
elements: { menubar },
builders: { createMenu }
} = createMenubar()
</script>
<div use:melt={$menubar}>
<slot {createMenu} />
</div>
@@ -0,0 +1,94 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge'
import Popover from '../Popover.svelte'
import { melt } from '@melt-ui/svelte'
import { createEventDispatcher } from 'svelte'
import SideBarNotification from './SideBarNotification.svelte'
import { goto } from '$app/navigation'
export let label: string | undefined = undefined
export let icon: any | undefined = undefined
export let isCollapsed: boolean
export let disabled: boolean = false
export let lightMode: boolean = false
export let stopPropagationOnClick: boolean = false
export let shortcut: string = ''
export let notificationsCount: number = 0
export let color: string | null = null
export let trigger: any | undefined = undefined
export let href: string | undefined = undefined
export let openHref: boolean = false
let dispatch = createEventDispatcher()
</script>
{#if !disabled}
<Popover appearTimeout={0} disappearTimeout={0} class="w-full" disablePopup={!isCollapsed}>
<button
on:click={(e) => {
if (stopPropagationOnClick) e.preventDefault()
if (openHref && href) {
goto(href)
}
dispatch('click')
}}
class={twMerge(
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
lightMode
? 'text-primary data-[highlighted]:bg-surface-hover hover:bg-surface-hover'
: 'data-[highlighted]:bg-[#2A3648] hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
color ? 'border-4' : '',
'transition-all',
$$props.class
)}
style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 4px);` : ''}
title={label}
use:melt={trigger}
>
{#if icon}
<svelte:component
this={icon}
size={16}
class={twMerge(
'flex-shrink-0',
lightMode
? 'text-primary group-hover:text-secondary'
: 'text-primary-inverse group-hover:text-secondary-inverse dark:group-hover:text-secondary dark:text-primary',
'transition-all'
)}
/>
{/if}
{#if !isCollapsed && label}
<span
class={twMerge(
'whitespace-pre truncate',
lightMode ? 'text-primary' : 'text-primary-inverse dark:text-primary',
'transition-all',
$$props.class
)}
>
{label}
<span class="pl-2 text-xs dark:text-secondary light:text-secondary-inverse font-semibold">
{shortcut}
</span>
</span>
{/if}
{#if isCollapsed && notificationsCount > 0}
<div class="absolute translate-x-1/2 translate-y-1/2 -top-2 right-1 flex h-fit w-fit">
<SideBarNotification notificationCount={notificationsCount} small={true} />
</div>
{:else if notificationsCount > 0}
<div class="ml-auto">
<SideBarNotification notificationCount={notificationsCount} small={false} />
</div>
{/if}
</button>
<svelte:fragment slot="text">
{#if label}
{label}
{/if}
</svelte:fragment>
</Popover>
{/if}
@@ -0,0 +1,86 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { navigating, page } from '$app/stores'
import Popover from '../Popover.svelte'
import { base } from '$app/paths'
import { melt } from '@melt-ui/svelte'
export let label: string
export let href: string
export let icon: any | undefined = undefined
export let isCollapsed: boolean
export let disabled: boolean = false
export let lightMode: boolean = false
export let item: any
let isSelected = false
navigating.subscribe(() => {
if (href === `${base}/`) {
isSelected = $page.url.pathname === href
} else {
isSelected = $page.url.pathname.startsWith(href)
}
})
</script>
{#if !disabled}
<Popover appearTimeout={0} disappearTimeout={0} class="w-full" disablePopup={!isCollapsed}>
<a
{href}
class={classNames(
'group flex items-center px-2 py-2 text-sm font-light rounded-md h-8 gap-3',
isSelected
? lightMode
? 'bg-surface-selected hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
: 'bg-frost-700 hover:bg-[#30404e] data-[highlighted]:bg-[#30404e]'
: lightMode
? 'hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
: 'hover:bg-[#2A3648] data-[highlighted]:bg-[#2A3648]',
'hover:transition-all',
$$props.class
)}
target={href.includes('http') ? '_blank' : null}
title={isCollapsed ? undefined : label}
use:melt={item}
>
{#if icon}
<svelte:component
this={icon}
size={16}
class={classNames(
'flex-shrink-0',
isSelected
? lightMode
? 'text-primary group-hover:text-secondary'
: 'text-frost-200 group-hover:text-white'
: lightMode
? 'text-primary group-hover:text-secondary'
: 'text-gray-100 group-hover:text-white',
'transition-all'
)}
/>
{/if}
{#if !isCollapsed}
<span
class={classNames(
'whitespace-pre truncate',
isSelected
? lightMode
? 'text-primary group-hover:text-secondary'
: 'text-frost-200 group-hover:text-white'
: lightMode
? 'text-primary group-hover:text-secondary'
: 'text-gray-100 group-hover:text-white',
'transition-all duration-75'
)}
>
{label}
</span>
{/if}
</a>
<svelte:fragment slot="text">
{label}
</svelte:fragment>
</Popover>
{/if}
@@ -35,9 +35,6 @@
AlertCircle,
Database
} from 'lucide-svelte'
import Menu from '../common/menu/MenuV2.svelte'
import MenuButton from './MenuButton.svelte'
import { MenuItem } from '@rgossiaux/svelte-headlessui'
import UserMenu from './UserMenu.svelte'
import DiscordIcon from '../icons/brands/Discord.svelte'
import { WorkspaceService } from '$lib/gen'
@@ -53,7 +50,12 @@
import SideBarNotification from './SideBarNotification.svelte'
import KafkaIcon from '../icons/KafkaIcon.svelte'
import NatsIcon from '../icons/NatsIcon.svelte'
import Menubar from '../meltComponents/Menubar.svelte'
import Menu from '../meltComponents/Menu.svelte'
import { melt } from '@melt-ui/svelte'
import MenuButtonMelt from './MenuButtonMelt.svelte'
import MenuSingleItem from '../meltComponents/MenuSingleItem.svelte'
import MenuLinkMelt from './MenuLinkMelt.svelte'
export let numUnacknowledgedCriticalAlerts = 0
$: mainMenuLinks = [
@@ -321,141 +323,182 @@
class:opacity-0={isCollapsed}>Triggers</div
>
<div class="space-y-1">
{#each triggerMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
<MenuLink class="!text-xs" {...menuLink} {isCollapsed} />
{/each}
{#if extraTriggerLinks.length > 0 && !$userStore?.operator}
<Menu>
<div
slot="trigger"
class="w-full text-gray-400 text-2xs flex flex-row gap-1 py-1 items-center px-2 hover:bg-[#2A3648] dark:hover:bg-[#30404e] rounded"
>
<Plus size={14} />
</div>
{#each extraTriggerLinks as subItem (subItem.href ?? subItem.label)}
<MenuItem>
<div class="py-1" role="none">
<a
href={subItem.disabled ? '' : subItem.href}
class={twMerge(
'text-secondary block px-4 py-2 text-2xs hover:bg-surface-hover hover:text-primary',
subItem.disabled ? 'pointer-events-none opacity-50' : ''
)}
role="menuitem"
tabindex="-1"
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
<svelte:component this={subItem.icon} size={16} />
{/if}
{subItem.label}
</div>
</a>
<Menubar let:createMenu>
{#each triggerMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
<MenuLink class="!text-xs" {...menuLink} {isCollapsed} />
{/each}
{#if extraTriggerLinks.length > 0 && !$userStore?.operator}
<Menu {createMenu} let:item>
<svelte:fragment slot="trigger" let:trigger>
<div
slot="trigger"
class={twMerge(
'w-full text-gray-400 text-2xs flex flex-row gap-1 py-1 items-center px-2 hover:bg-[#2A3648] dark:hover:bg-[#30404e] rounded',
'data-[highlighted]:bg-[#2A3648] dark:data-[highlighted]:bg-[#30404e]'
)}
use:melt={trigger}
>
<Plus size={14} />
</div>
</MenuItem>
{/each}
</Menu>
{/if}
</svelte:fragment>
{#each extraTriggerLinks as subItem (subItem.href ?? subItem.label)}
<div>
<div class="py-1" role="none">
<a
href={subItem.disabled ? '' : subItem.href}
class={twMerge(
'text-secondary block px-4 py-2 text-2xs hover:bg-surface-hover hover:text-primary',
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary',
subItem.disabled ? 'pointer-events-none opacity-50' : ''
)}
role="menuitem"
tabindex="-1"
use:melt={item}
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
<svelte:component this={subItem.icon} size={16} />
{/if}
{subItem.label}
</div>
</a>
</div>
</div>
{/each}
</Menu>
{/if}
</Menubar>
</div>
</div>
</div>
<div class="flex flex-col h-full justify-end">
<div class={twMerge('space-y-0.5 mb-6 md:mb-10', noGap ? 'md:mb-0 mb-0' : 'mb-6 md:mb-10')}>
<UserMenu {isCollapsed} />
{#each secondaryMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
{#if menuLink.subItems}
{@const notificationsCount = computeAllNotificationsCount(menuLink.subItems)}
<Menu>
<div slot="trigger">
<MenuButton class="!text-2xs" {...menuLink} {isCollapsed} {notificationsCount} />
<Menubar let:createMenu>
<UserMenu {isCollapsed} {createMenu} />
{#each secondaryMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
{#if menuLink.subItems}
{@const notificationsCount = computeAllNotificationsCount(menuLink.subItems)}
<Menu {createMenu} let:item>
<svelte:fragment slot="trigger" let:trigger>
<MenuButtonMelt
class="!text-2xs"
{...menuLink}
{isCollapsed}
{notificationsCount}
{trigger}
/>
</svelte:fragment>
<div>
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
<div class="py-1" role="none">
{#if subItem?.['action']}
<button
class="text-secondary font-normal w-full block px-4 py-2 text-2xs data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary"
on:click={subItem?.['action']}
use:melt={item}
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
<svelte:component this={subItem.icon} size={16} />
{/if}
{subItem.label}
{#if subItem?.['notificationCount']}
<div class="ml-auto">
<SideBarNotification
notificationCount={subItem['notificationCount']}
/>
</div>
{/if}
</div>
</button>
{:else}
<a
href={subItem.href}
class="text-secondary font-normal block px-4 py-2 text-2xs data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary"
role="menuitem"
tabindex="-1"
use:melt={item}
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
<svelte:component this={subItem.icon} size={16} />
{/if}
{subItem.label}
{#if subItem?.['notificationCount']}
<div class="ml-auto">
<SideBarNotification
notificationCount={subItem['notificationCount']}
/>
</div>
{/if}
</div>
</a>
{/if}
</div>
{/each}
</div>
</Menu>
{:else}
<div>
<MenuSingleItem {createMenu} let:item>
<svelte:fragment slot="trigger" let:trigger>
<div class="w-full">
<MenuButtonMelt
class="!text-2xs"
{...menuLink}
{isCollapsed}
{trigger}
openHref={true}
/>
</div>
</svelte:fragment>
<MenuLinkMelt class="!text-2xs" {...menuLink} {isCollapsed} {item} />
</MenuSingleItem>
</div>
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
<MenuItem>
<div class="py-1" role="none">
{#if subItem?.['action']}
<button
class="text-secondary font-normal w-full block px-4 py-2 text-2xs hover:bg-surface-hover hover:text-primary"
on:click={subItem?.['action']}
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
<svelte:component this={subItem.icon} size={16} />
{/if}
{subItem.label}
{#if subItem?.['notificationCount']}
<div class="ml-auto">
<SideBarNotification notificationCount={subItem['notificationCount']} />
</div>
{/if}
</div>
</button>
{:else}
<a
href={subItem.href}
class="text-secondary font-normal block px-4 py-2 text-2xs hover:bg-surface-hover hover:text-primary"
role="menuitem"
tabindex="-1"
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
<svelte:component this={subItem.icon} size={16} />
{/if}
{subItem.label}
{#if subItem?.['notificationCount']}
<div class="ml-auto">
<SideBarNotification notificationCount={subItem['notificationCount']} />
</div>
{/if}
</div>
</a>
{/if}
</div>
</MenuItem>
{/each}
</Menu>
{:else}
<MenuLink class="!text-2xs" {...menuLink} {isCollapsed} />
{/if}
{/each}
{/if}
{/each}
</Menubar>
</div>
<div class="space-y-0.5">
{#each thirdMenuLinks as menuLink (menuLink)}
{#if menuLink.subItems}
<Menu>
<div slot="trigger">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="relative"
on:click={() => {
if (menuLink.label === 'Help') {
openChangelogs()
}
}}
>
<MenuButton class="!text-2xs" {...menuLink} {isCollapsed} />
{#if menuLink.label === 'Help' && hasNewChangelogs}
<span class="absolute top-1 right-1 flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-frost-400 opacity-75"
/>
<span class="relative inline-flex rounded-full h-2 w-2 bg-frost-500" />
</span>
{/if}
</div>
</div>
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
<MenuItem>
<Menubar let:createMenu>
{#each thirdMenuLinks as menuLink (menuLink)}
{#if menuLink.subItems}
<Menu {createMenu} let:item>
<svelte:fragment slot="trigger" let:trigger>
<button
class="relative w-full"
on:click={() => {
if (menuLink.label === 'Help') {
openChangelogs()
}
}}
>
<MenuButtonMelt class="!text-2xs" {...menuLink} {isCollapsed} {trigger} />
{#if menuLink.label === 'Help' && hasNewChangelogs}
<span class="absolute top-1 right-1 flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-frost-400 opacity-75"
/>
<span class="relative inline-flex rounded-full h-2 w-2 bg-frost-500" />
</span>
{/if}
</button>
</svelte:fragment>
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
<div class="py-1" role="none">
<a
href={subItem.href}
class="text-secondary block px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary relative"
class={twMerge(
'text-secondary block px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary relative',
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
)}
role="menuitem"
tabindex="-1"
target="_blank"
use:melt={item}
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
@@ -466,13 +509,11 @@
</div>
</a>
</div>
</MenuItem>
{/each}
{#if recentChangelogs.length > 0}
<div class="w-full h-1 border-t" />
<span class="text-xs px-4 font-bold"> Latest changelogs </span>
{#each recentChangelogs as changelog}
<MenuItem>
{/each}
{#if recentChangelogs.length > 0}
<div class="w-full h-1 border-t" />
<span class="text-xs px-4 font-bold"> Latest changelogs </span>
{#each recentChangelogs as changelog}
<div class="py-1" role="none">
<a
href={changelog.href}
@@ -480,18 +521,19 @@
role="menuitem"
tabindex="-1"
target="_blank"
use:melt={item}
>
<div class="flex flex-row items-center gap-2">
{changelog.label}
</div>
</a>
</div>
</MenuItem>
{/each}
{/if}
</Menu>
{/if}
{/each}
{/each}
{/if}
</Menu>
{/if}
{/each}
</Menubar>
</div>
</div>
</nav>
@@ -8,30 +8,32 @@
isPremiumStore,
workspaceStore
} from '$lib/stores'
import Menu from '../common/menu/MenuV2.svelte'
import { USER_SETTINGS_HASH } from './settings'
import { isCloudHosted } from '$lib/cloud'
import { twMerge } from 'tailwind-merge'
import { Crown, HardHat, LogOut, Moon, Settings, Sun, User } from 'lucide-svelte'
import DarkModeObserver from '../DarkModeObserver.svelte'
import MenuButton from './MenuButton.svelte'
import { MenuItem } from '@rgossiaux/svelte-headlessui'
import MenuButtonMelt from './MenuButtonMelt.svelte'
import { melt } from '@melt-ui/svelte'
import Menu from '$lib/components/meltComponents/Menu.svelte'
let darkMode: boolean = false
export let isCollapsed: boolean = false
export let lightMode: boolean = false
export let createMenu: (any) => any
</script>
<Menu>
<div slot="trigger" class="w-full">
<MenuButton
<Menu {createMenu} let:item>
<svelte:fragment slot="trigger" let:trigger>
<MenuButtonMelt
class="!text-xs"
icon={User}
label={`User (${$userStore?.username ?? $userStore?.email})`}
{isCollapsed}
{lightMode}
{trigger}
/>
</div>
</svelte:fragment>
<div class="divide-y z-20">
<div class="px-4 py-3" role="none">
<p class="text-sm font-medium text-primary truncate" role="none">
@@ -47,17 +49,18 @@
</div>
<div class="py-1" role="none">
<MenuItem
<a
href={USER_SETTINGS_HASH}
class={twMerge(
'flex flex-row gap-2 items-center px-4 py-2 ',
'text-secondary text-sm',
'hover:bg-surface-hover hover:text-primary cursor-pointer'
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
)}
use:melt={item}
>
<Settings size={14} />
Account settings
</MenuItem>
</a>
</div>
<div class="py-1" role="none">
@@ -72,11 +75,13 @@
}
}}
class={twMerge(
'text-secondary block text-left px-4 py-2 font-normal text-sm hover:bg-surface-hover hover:text-primary w-full',
'flex flex-row items-center gap-2'
'text-secondary block text-left px-4 py-2 font-normal text-sm w-full',
'flex flex-row items-center gap-2',
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
)}
role="menuitem"
tabindex="-1"
use:melt={item}
>
{#if darkMode}
<Sun size={14} />
@@ -88,18 +93,18 @@
</div>
<div class="py-1" role="none">
<MenuItem
href="#"
<button
on:click={() => logout()}
class={twMerge(
'flex flex-row gap-2 items-center px-4 py-2 ',
'flex flex-row gap-2 items-center px-4 py-2 w-full',
'text-secondary text-sm',
'hover:bg-surface-hover hover:text-primary cursor-pointer'
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
)}
use:melt={item}
>
<LogOut size={14} />
Sign out
</MenuItem>
</button>
</div>
{#if isCloudHosted()}