feat: action link (#769)

This commit is contained in:
Jason Rasmussen
2026-09-02 15:40:46 -04:00
committed by GitHub
parent 0fe36de700
commit 91114a8a11
4 changed files with 55 additions and 16 deletions
+5 -1
View File
@@ -7,6 +7,7 @@ import {
toastManager,
type ActionItem,
type ActionItemHandler,
type ActionLink,
type CarouselImageItem,
type IconLike,
type MenuItems,
@@ -64,6 +65,7 @@ import {
mdiMessageAlert,
mdiMessageAlertOutline,
mdiNumeric,
mdiOpenInNew,
mdiPalette,
mdiPanVertical,
mdiPartyPopper,
@@ -215,7 +217,8 @@ export const ExampleActions = {
Edit: { title: 'Edit album', icon: mdiPencilOutline, onAction },
Download: { title: 'Download', icon: mdiDownload, onAction },
Delete: { title: 'Delete', icon: mdiTrashCanOutline, color: 'danger', onAction },
} satisfies Record<string, ActionItem>;
Link: { title: 'Link', icon: mdiOpenInNew, href: 'https://immich.app/' },
} satisfies Record<string, ActionItem | ActionLink>;
export const exampleActions: ActionItem[] = [
ExampleActions.Share,
@@ -230,6 +233,7 @@ export const exampleMenuItems: MenuItems = [
ExampleActions.Copy,
ExampleActions.Edit,
ExampleActions.Download,
ExampleActions.Link,
MenuItemType.Divider,
ExampleActions.Delete,
];
@@ -1,10 +1,11 @@
<script lang="ts">
import Icon from '$lib/components/Icon/Icon.svelte';
import Link from '$lib/components/Link/Link.svelte';
import Text from '$lib/components/Text/Text.svelte';
import { zIndex } from '$lib/constants.js';
import { styleVariants } from '$lib/styles.js';
import { type ActionItem, type ContextMenuProps, type MenuItems } from '$lib/types.js';
import { isEnabled, isMenuItemType } from '$lib/utilities/common.js';
import { type ActionItem, type ActionLink, type ContextMenuProps, type MenuItems } from '$lib/types.js';
import { isActionLink, isEnabled, isMenuItemType } from '$lib/utilities/common.js';
import { cleanClass } from '$lib/utilities/internal.js';
import { DropdownMenu } from 'bits-ui';
import { fly } from 'svelte/transition';
@@ -117,6 +118,13 @@
const { side, align } = $derived(getAlignment(position));
</script>
{#snippet itemText(item: ActionItem | ActionLink)}
{#if item.icon}
<Icon icon={item.icon} class="m-2 shrink-0" />
{/if}
<Text class="grow text-start font-medium select-none" size="medium">{item.title}</Text>
{/snippet}
<DropdownMenu.Root open={true} onOpenChange={() => onClose()}>
<DropdownMenu.Portal>
<DropdownMenu.Content forceMount customAnchor={target} {side} {align} {alignOffset} {sideOffset}>
@@ -127,6 +135,19 @@
{#each filteredItems as item, i (isMenuItemType(item) ? i : item.title)}
{#if isMenuItemType(item)}
<DropdownMenu.Separator class="dark:border-light-300 my-0.5 border-t" />
{:else if isActionLink(item)}
<DropdownMenu.Item>
{#snippet child({ props })}
<Link
underline={false}
{...props}
href={item.href}
class={itemStyles({ color: item.color, inset: true })}
>
{@render itemText(item)}
</Link>
{/snippet}
</DropdownMenu.Item>
{:else}
<DropdownMenu.Item
textValue={item.title}
@@ -134,10 +155,7 @@
onSelect={() => item.onAction(item)}
class={itemStyles({ color: item.color, inset: true })}
>
{#if item.icon}
<Icon icon={item.icon} class="m-2 shrink-0" />
{/if}
<Text class="grow text-start font-medium select-none" size="medium">{item.title}</Text>
{@render itemText(item)}
</DropdownMenu.Item>
{/if}
{/each}
+15 -7
View File
@@ -330,7 +330,8 @@ export enum MenuItemType {
Divider = 'divider',
}
export type MenuItems = Array<ActionItem | MenuItemType | undefined>;
export type MenuItem = ActionItem | ActionLink | MenuItemType | undefined;
export type MenuItems = MenuItem[];
export type MenuProps = {
items: MenuItems;
@@ -376,13 +377,23 @@ export type ActionItemTag = {
class?: string;
};
export type ActionItem = {
type ActionBase = IfLike & {
title: string;
description?: string;
color?: Color;
icon?: IconLike;
iconClass?: string;
};
export type ActionLink = ActionBase & {
href: string;
};
export type ActionItem = ActionBase & {
/**
renders as badges on the item
*/
tags?: Array<string | ActionItemTag>;
description?: string;
/**
searchable text in addition to title, tags, and description
*/
@@ -391,13 +402,10 @@ export type ActionItem = {
text that should be highlighted as a match
*/
highlights?: string[];
icon?: IconLike;
iconClass?: string;
color?: Color;
onAction: ActionItemHandler;
shortcuts?: MaybeArray<Shortcut>;
shortcutOptions?: { ignoreInputFields?: boolean; preventDefault?: boolean };
} & IfLike;
};
export type BreadcrumbsProps = {
separator?: IconLike | { text: string };
+11 -2
View File
@@ -1,5 +1,12 @@
import { goto } from '$app/navigation';
import { MenuItemType, type ActionItem, type GithubLinkProps, type GithubLinkType, type IfLike } from '$lib/types.js';
import {
MenuItemType,
type ActionLink,
type GithubLinkProps,
type GithubLinkType,
type IfLike,
type MenuItem,
} from '$lib/types.js';
import type { DateTime } from 'luxon';
export type QueryValue = number | string;
@@ -118,10 +125,12 @@ export type ArticleMetadata = {
tags?: string[];
};
export const isMenuItemType = (item: ActionItem | MenuItemType): item is MenuItemType => {
export const isMenuItemType = (item: MenuItem): item is MenuItemType => {
return item === MenuItemType.Divider;
};
export const isActionLink = (item: MenuItem): item is ActionLink => typeof item === 'object' && 'href' in item;
export const resolveMetadata = (site: Metadata, page?: Metadata, article?: ArticleMetadata) => {
const title = page ? `${page.title} | ${site.title}` : site.title;
const description = page?.description ?? site.description;