mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-20 16:03:21 +00:00
* feat: add workspace mentions to AI composer * fix: ignore embedded mention text in composer * fix: sync only standalone composer mentions * fix: keep punctuation-adjacent mentions active * fix: use unicode mention boundaries * fix: handle unicode mention prefixes * fix: avoid rescanning mention prefixes
122 lines
4.3 KiB
Svelte
122 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import MenuItem from '$lib/components/meltComponents/MenuItem.svelte'
|
|
import Toggle from '$lib/components/Toggle.svelte'
|
|
import DropdownSubmenuItem from '$lib/components/DropdownSubmenuItem.svelte'
|
|
import { Check, Loader2 } from 'lucide-svelte'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import type { MenubarMenuElements, createDropdownMenu } from '@melt-ui/svelte'
|
|
import type { Item } from '$lib/utils'
|
|
import { Tooltip } from './meltComponents'
|
|
|
|
interface Props {
|
|
aiId?: string
|
|
items?: Item[] | (() => Item[]) | (() => Promise<Item[]>)
|
|
meltItem: MenubarMenuElements['item']
|
|
builders?: ReturnType<typeof createDropdownMenu>['builders']
|
|
close?: (afterClose?: () => void) => void
|
|
}
|
|
|
|
let { aiId, items = [], meltItem, builders, close }: Props = $props()
|
|
|
|
let computedItems: Item[] | undefined = $state(undefined)
|
|
async function computeItems() {
|
|
if (typeof items === 'function') {
|
|
computedItems = ((await items()) ?? []).filter((item) => !item.hide)
|
|
} else {
|
|
computedItems = items.filter((item) => !item.hide)
|
|
}
|
|
}
|
|
|
|
computeItems()
|
|
</script>
|
|
|
|
{#snippet menuItem(item: Item)}
|
|
<MenuItem
|
|
onClick={(e) => item?.action?.(e)}
|
|
href={item?.href}
|
|
target={item?.hrefTarget}
|
|
disabled={item?.disabled}
|
|
class={twMerge(
|
|
'px-4 py-2 text-primary font-normal hover:bg-surface-hover cursor-pointer text-xs transition-colors w-full',
|
|
'data-[highlighted]:bg-surface-hover',
|
|
'flex flex-row gap-2 items-center rounded-sm',
|
|
// `pointer-events-none` lets a hover fall through the natively-disabled button to the
|
|
// wrapper below, so a disabled item's `title` tooltip can still explain *why* it's disabled.
|
|
item?.disabled && 'text-disabled cursor-not-allowed pointer-events-none',
|
|
item?.type === 'delete' &&
|
|
!item?.disabled &&
|
|
'text-red-600 dark:text-red-400 data-[highlighted]:bg-red-500/10 dark:data-[highlighted]:bg-red-900/80 dark:data-[highlighted]:text-red-300 '
|
|
)}
|
|
item={meltItem}
|
|
aiId={`${aiId ? `${aiId}-${item.displayName}` : undefined}`}
|
|
aiDescription={item.displayName}
|
|
>
|
|
{#if item.icon}
|
|
<item.icon size={14} color={item.iconColor} class="shrink-0" {...item.iconProps ?? {}} />
|
|
{/if}
|
|
<div class="grow min-w-0 text-left">
|
|
<p
|
|
title={item.disabled && item.tooltip ? undefined : item.displayName}
|
|
class="truncate whitespace-nowrap"
|
|
>
|
|
{item.displayName}
|
|
</p>
|
|
{#if item.description}
|
|
<p class="text-2xs text-secondary">{item.description}</p>
|
|
{/if}
|
|
</div>
|
|
{@render item.extra?.()}
|
|
{#if item.shortcut || item.selected || item.toggle !== undefined}
|
|
<!-- Single trailing group so `shortcut` and `selected` can coexist:
|
|
two `ml-auto` siblings would collapse to one right-aligned item. -->
|
|
<div class="ml-auto flex shrink-0 items-center gap-2">
|
|
{#if item.shortcut}
|
|
<span class="pl-4 text-2xs text-secondary">{item.shortcut}</span>
|
|
{/if}
|
|
{#if item.selected}
|
|
<Check size={14} class="text-primary" />
|
|
{/if}
|
|
{#if item.toggle !== undefined}
|
|
<!-- Indicator only: the click belongs to the row, so the switch must not
|
|
take it (nor answer for the row to a screen reader). -->
|
|
<span class="pointer-events-none" aria-hidden="true">
|
|
<Toggle size="2xs" checked={item.toggle} />
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{#if item.tooltip && !item.disabled}
|
|
<!-- Enabled items get the rich ⓘ tooltip. Disabled items can't (a native disabled button
|
|
swallows subtree pointer events), so they fall back to the wrapper `title` below. -->
|
|
<Tooltip>
|
|
{#snippet text()}
|
|
{item.tooltip}
|
|
{/snippet}
|
|
</Tooltip>
|
|
{/if}
|
|
</MenuItem>
|
|
{/snippet}
|
|
|
|
{#if computedItems}
|
|
<div class="flex flex-col">
|
|
{#each computedItems ?? [] as item}
|
|
{#if item.separatorTop}
|
|
<div class="my-1 border-t border-border-light"></div>
|
|
{/if}
|
|
{#if (item.submenuItems || item.customSubmenu) && builders}
|
|
<DropdownSubmenuItem {item} {builders} {meltItem} close={close ?? (() => {})} />
|
|
{:else if item.disabled && item.tooltip}
|
|
<!-- Wrapper carries the native `title`; the disabled button's `pointer-events-none`
|
|
lets the hover reach it so the user learns why the item is disabled. -->
|
|
<div title={item.tooltip} class="w-full">
|
|
{@render menuItem(item)}
|
|
</div>
|
|
{:else}
|
|
{@render menuItem(item)}
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<Loader2 class="animate-spin mx-auto p-4" size={24} />
|
|
{/if}
|