add a MenuItem component

This commit is contained in:
Guilhem
2025-02-10 20:09:26 +00:00
parent 805478790c
commit 0594838c36
4 changed files with 188 additions and 235 deletions

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { melt } from '@melt-ui/svelte'
export let href: string | undefined = undefined
export let disabled: boolean = false
export let target: string | undefined = undefined
export let item: any
</script>
{#if href}
<a
use:melt={item}
{href}
class={$$props.class}
role="menuitem"
aria-disabled={disabled}
tabindex={disabled ? -1 : undefined}
{target}
>
<slot />
</a>
{:else}
<button use:melt={item} on:click {disabled} class={$$props.class} role="menuitem">
<slot />
</button>
{/if}

View File

@@ -1,3 +1,4 @@
export { default as Menu } from './Menu.svelte'
export { default as Menubar } from './Menubar.svelte'
export { default as MenuSingleItem } from './MenuSingleItem.svelte'
export { default as MenuItem } from './MenuItem.svelte'

View File

@@ -50,7 +50,7 @@
import SideBarNotification from './SideBarNotification.svelte'
import KafkaIcon from '../icons/KafkaIcon.svelte'
import NatsIcon from '../icons/NatsIcon.svelte'
import { Menubar, Menu, MenuSingleItem } from '$lib/components/meltComponents'
import { Menubar, Menu, MenuSingleItem, MenuItem } from '$lib/components/meltComponents'
import { melt } from '@melt-ui/svelte'
import MenuButtonMelt from './MenuButtonMelt.svelte'
import MenuLinkMelt from './MenuLinkMelt.svelte'
@@ -301,6 +301,10 @@
}
return count
}
const itemClass = twMerge(
'text-secondary font-normal w-full block px-4 py-2 text-2xs data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
)
</script>
<nav
@@ -340,29 +344,22 @@
</div>
</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}
data-disabled={subItem.disabled ? true : undefined}
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
<svelte:component this={subItem.icon} size={16} />
{/if}
{subItem.label}
</div>
</a>
<MenuItem
href={subItem.disabled ? '' : subItem.href}
class={twMerge(
itemClass,
subItem.disabled ? 'pointer-events-none opacity-50' : ''
)}
{item}
disabled={subItem.disabled}
>
<div class="flex flex-row items-center gap-2">
{#if subItem.icon}
<svelte:component this={subItem.icon} size={16} />
{/if}
{subItem.label}
</div>
</div>
</MenuItem>
{/each}
</Menu>
{/if}
@@ -372,8 +369,9 @@
</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')}>
<Menubar let:createMenu>
<Menubar let:createMenu class="flex flex-col gap-1">
<UserMenu {isCollapsed} {createMenu} />
{#each secondaryMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
{#if menuLink.subItems}
{@const notificationsCount = computeAllNotificationsCount(menuLink.subItems)}
@@ -387,82 +385,51 @@
{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>
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
<MenuItem
class={itemClass}
href={subItem.href}
{item}
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>
{/each}
</div>
</MenuItem>
{/each}
</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>
<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>
{/if}
{/each}
</Menubar>
</div>
<div class="space-y-0.5">
<Menubar let:createMenu>
<Menubar let:createMenu class="flex flex-col gap-1">
{#each thirdMenuLinks as menuLink (menuLink)}
{#if menuLink.subItems}
<Menu {createMenu} let:item usePointerDownOutside>
@@ -487,46 +454,25 @@
</button>
</svelte:fragment>
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
<div class="py-1" role="none">
<a
href={subItem.href}
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}
<svelte:component this={subItem.icon} size={16} />
{/if}
<MenuItem href={subItem.href} class={itemClass} target="_blank" {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>
{subItem.label}
</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}
<div class="py-1" role="none">
<a
href={changelog.href}
class="text-secondary block px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary relative"
role="menuitem"
tabindex="-1"
target="_blank"
use:melt={item}
>
<div class="flex flex-row items-center gap-2">
{changelog.label}
</div>
</a>
</div>
<MenuItem href={changelog.href} class={itemClass} target="_blank" {item}>
<div class="flex flex-row items-center gap-2">
{changelog.label}
</div>
</MenuItem>
{/each}
{/if}
</Menu>

View File

@@ -14,13 +14,19 @@
import { Crown, HardHat, LogOut, Moon, Settings, Sun, User } from 'lucide-svelte'
import DarkModeObserver from '../DarkModeObserver.svelte'
import MenuButtonMelt from './MenuButtonMelt.svelte'
import { Menu, MenuItem } from '$lib/components/meltComponents'
import { melt } from '@melt-ui/svelte'
import { Menu } from '$lib/components/meltComponents'
let darkMode: boolean = false
export let isCollapsed: boolean = false
export let lightMode: boolean = false
export let createMenu: (any) => any
const itemClass = twMerge(
'text-secondary text-left font-normal text-xs ',
'flex flex-row items-center gap-2 px-4 py-3 w-full',
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
)
</script>
<Menu {createMenu} let:item usePointerDownOutside>
@@ -34,130 +40,104 @@
{trigger}
/>
</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">
{$userStore?.email}
</p>
<span class="text-xs text-tertiary flex flex-row gap-2 items-center">
{#if $userStore?.is_admin}
Admin of this workspace <Crown size={14} />
{:else if $userStore?.operator}
Operator in this workspace <HardHat size={14} />
{/if}
</span>
</div>
<div class="py-1" role="none">
<a
href={USER_SETTINGS_HASH}
class={twMerge(
'flex flex-row gap-2 items-center px-4 py-2 ',
'text-secondary text-sm',
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
)}
use:melt={item}
>
<Settings size={14} />
Account settings
</a>
</div>
<div class="px-4 py-3 border-b" role="none">
<p class="text-sm font-medium text-primary truncate" role="none">
{$userStore?.email}
</p>
<span class="text-xs text-tertiary flex flex-row gap-2 items-center">
{#if $userStore?.is_admin}
Admin of this workspace <Crown size={14} />
{:else if $userStore?.operator}
Operator in this workspace <HardHat size={14} />
{/if}
</span>
</div>
<div class="py-1">
<MenuItem href={USER_SETTINGS_HASH} class={itemClass} {item}>
<Settings size={16} />
Account settings
</MenuItem>
<div class="py-1" role="none">
<button
on:click={() => {
if (!document.documentElement.classList.contains('dark')) {
document.documentElement.classList.add('dark')
window.localStorage.setItem('dark-mode', 'dark')
} else {
document.documentElement.classList.remove('dark')
window.localStorage.setItem('dark-mode', 'light')
}
}}
class={twMerge(
'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} />
{:else}
<Moon size={14} />
{/if}
Switch theme
</button>
</div>
<MenuItem
on:click={() => {
if (!document.documentElement.classList.contains('dark')) {
document.documentElement.classList.add('dark')
window.localStorage.setItem('dark-mode', 'dark')
} else {
document.documentElement.classList.remove('dark')
window.localStorage.setItem('dark-mode', 'light')
}
}}
class={itemClass}
{item}
>
{#if darkMode}
<Sun size={16} />
{:else}
<Moon size={16} />
{/if}
Switch theme
</MenuItem>
<div class="py-1" role="none">
<button
on:click={() => logout()}
class={twMerge(
'flex flex-row gap-2 items-center px-4 py-2 w-full',
'text-secondary text-sm',
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
)}
use:melt={item}
>
<LogOut size={14} />
Sign out
</button>
</div>
<MenuItem
on:click={() => logout()}
class={twMerge(itemClass, 'text-primary font-semibold')}
{item}
>
<LogOut size={16} />
Sign out
</MenuItem>
</div>
{#if isCloudHosted()}
{#if isCloudHosted() || true}
<div class="border-t">
{#if !$isPremiumStore}
<div class="py-1" role="none">
<span class="text-secondary block w-full text-left px-4 py-2 text-sm"
>{$usageStore}/1000 user execs</span
>
<div class="w-full bg-gray-200 h-1">
<div class="bg-blue-400 h-1" style="width: {Math.min($usageStore, 1000) / 10}%" />
<span class="text-secondary block w-full text-left px-4 py-2 text-xs"
>{$usageStore}/1000 user execs</span
>
<div class="px-4 w-full h-1 mb-1">
<div class="bg-gray-200 h-full rounded-sm overflow-hidden">
<div class="bg-blue-400 h-full" style="width: {Math.min($usageStore, 1000) / 10}%" />
</div>
{#if $workspaceStore != 'demo'}
<span class="text-secondary block w-full text-left px-4 py-2 text-xs"
>{$workspaceUsageStore}/1000 free workspace execs</span
>
<div class="w-full bg-gray-200 h-1">
</div>
{#if $workspaceStore != 'demo'}
<span class="text-secondary block w-full text-left px-4 py-2 text-xs"
>{$workspaceUsageStore}/1000 free workspace execs</span
>
<div class="px-4 w-full h-1 mb-1">
<div class="bg-gray-200 h-full rounded-sm overflow-hidden">
<div
class="bg-blue-400 h-1"
class="bg-blue-400 h-full"
style="width: {Math.min($workspaceUsageStore, 1000) / 10}%"
/>
</div>
{/if}
{#if $userStore?.is_admin}
<button
type="button"
class="text-secondary block font-normal w-full text-left px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
role="menuitem"
tabindex="-1"
on:click={() => {
goto('/workspace_settings?tab=premium')
}}
>
Upgrade
</button>
{/if}
</div>
{:else}
<div class="py-1" role="none">
<button
type="button"
class="text-secondary block font-normal w-full text-left px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
role="menuitem"
tabindex="-1"
</div>
{/if}
{#if $userStore?.is_admin || true}
<MenuItem
class={twMerge(itemClass, 'py-2')}
on:click={() => {
goto('/workspace_settings?tab=premium')
}}
{item}
>
Premium plan
</button>
</div>
Upgrade
</MenuItem>
{/if}
{:else}
<MenuItem
class={twMerge(itemClass, 'py-2')}
on:click={() => {
goto('/workspace_settings?tab=premium')
}}
{item}
>
Premium plan
</MenuItem>
{/if}
{/if}
</div>
</div>
{/if}
</Menu>
<DarkModeObserver bind:darkMode />