mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-07 18:29:04 +02:00
feat: pin table row actions and add them to details pages
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<script lang="ts" generics="T">
|
||||
import {
|
||||
buttonVariants,
|
||||
type ButtonSize,
|
||||
type ButtonVariant
|
||||
} from '#lib/components/ui/button/index.ts';
|
||||
import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js';
|
||||
import { m } from '#lib/paraglide/messages.js';
|
||||
import type { AdvancedTableAction } from '#lib/types/advanced-table.type.ts';
|
||||
import { LucideEllipsis } from '@lucide/svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
actions,
|
||||
label = m.toggle_menu(),
|
||||
variant = 'ghost',
|
||||
size = 'icon'
|
||||
}: {
|
||||
item: T;
|
||||
actions: AdvancedTableAction<T>[];
|
||||
label?: string;
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
} = $props();
|
||||
|
||||
let visibleActions = $derived(actions.filter((a) => !a.hidden));
|
||||
</script>
|
||||
|
||||
{#if visibleActions.length > 0}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class={buttonVariants({ variant, size })}>
|
||||
<LucideEllipsis class="size-4" />
|
||||
<span class="sr-only">{label}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
{#each visibleActions as action (action.label)}
|
||||
<DropdownMenu.Item
|
||||
onclick={() => action.onClick(item)}
|
||||
disabled={action.disabled}
|
||||
variant={action.variant === 'danger' ? 'destructive' : 'default'}
|
||||
>
|
||||
{#if action.icon}
|
||||
{@const Icon = action.icon}
|
||||
<Icon class="mr-2 size-4" />
|
||||
{/if}
|
||||
{action.label}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
{/if}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" generics="T extends {id:string}">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page as currentPage } from '$app/state';
|
||||
import ActionsMenu from '#lib/components/actions-menu.svelte';
|
||||
import Checkbox from '#lib/components/ui/checkbox/checkbox.svelte';
|
||||
import * as Pagination from '#lib/components/ui/pagination/index.ts';
|
||||
import * as Select from '#lib/components/ui/select/index.ts';
|
||||
@@ -13,12 +14,11 @@
|
||||
} from '#lib/types/advanced-table.type.ts';
|
||||
import type { ListRequestOptions, Paginated, SortRequest } from '#lib/types/list-request.type.ts';
|
||||
import { cn } from '#lib/utils/style.ts';
|
||||
import { ChevronDown, LucideEllipsis } from '@lucide/svelte';
|
||||
import { ChevronDown } from '@lucide/svelte';
|
||||
import { PersistedState } from 'runed';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Button, { buttonVariants } from '../ui/button/button.svelte';
|
||||
import * as DropdownMenu from '../ui/dropdown-menu/index.js';
|
||||
import Button from '../ui/button/button.svelte';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
import AdvancedTableToolbar from './advanced-table-toolbar.svelte';
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div in:fade>
|
||||
<Table.Root class="min-w-full table-auto overflow-x-auto">
|
||||
<Table.Root class="min-w-full table-auto">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
{#if selectedIds}
|
||||
@@ -263,7 +263,7 @@
|
||||
</Table.Head>
|
||||
{/each}
|
||||
{#if actions}
|
||||
<Table.Head align="right" class="w-12">
|
||||
<Table.Head align="right" class="bg-card sticky right-0 z-10 w-12">
|
||||
<span class="sr-only">{m.actions()}</span>
|
||||
</Table.Head>
|
||||
{/if}
|
||||
@@ -273,6 +273,7 @@
|
||||
{#each items.data as item (item.id)}
|
||||
<Table.Row
|
||||
class={{
|
||||
'group/row': true,
|
||||
'bg-muted/20': selectedIds?.includes(item.id),
|
||||
'cursor-pointer': getPrimaryAction(item) || onRowClick
|
||||
}}
|
||||
@@ -301,30 +302,12 @@
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
{#if actions}
|
||||
<Table.Cell align="right" class="w-12 py-0">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class={buttonVariants({ variant: 'ghost', size: 'icon' })}
|
||||
>
|
||||
<LucideEllipsis class="size-4" />
|
||||
<span class="sr-only">{m.toggle_menu()}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
{#each actions(item).filter((a) => !a.hidden) as action (action.label)}
|
||||
<DropdownMenu.Item
|
||||
onclick={() => action.onClick(item)}
|
||||
disabled={action.disabled}
|
||||
class={action.variant === 'danger' ? 'text-red-500!' : ''}
|
||||
>
|
||||
{#if action.icon}
|
||||
{@const Icon = action.icon}
|
||||
<Icon class="mr-2 size-4" />
|
||||
{/if}
|
||||
{action.label}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<!-- Pinned to the right edge so the actions stay reachable when the table scrolls horizontally -->
|
||||
<Table.Cell
|
||||
align="right"
|
||||
class="bg-card sticky right-0 z-10 w-12 py-0 transition-colors group-hover/row:bg-[color-mix(in_oklab,var(--color-muted)_50%,var(--color-card))]"
|
||||
>
|
||||
<ActionsMenu {item} actions={actions(item)} />
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
</Table.Row>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover/70 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground! relative z-50 min-w-48 animate-none! overflow-x-hidden overflow-y-auto rounded-3xl p-1.5 shadow-lg ring-1 duration-100 outline-none before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 data-closed:overflow-hidden',
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-popover/70 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:data-highlighted:bg-destructive/10! dark:**:data-[variant=destructive]:data-highlighted:bg-destructive/20! relative z-50 min-w-48 animate-none! overflow-x-hidden overflow-y-auto rounded-3xl p-1.5 shadow-lg ring-1 duration-100 outline-none before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 data-closed:overflow-hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
bind:ref
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
class={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground bg-popover/70 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground! relative w-auto min-w-36 animate-none! rounded-3xl p-1.5 shadow-lg ring-1 duration-100 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150',
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/5 dark:ring-foreground/10 text-popover-foreground bg-popover/70 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:data-highlighted:bg-destructive/10! dark:**:data-[variant=destructive]:data-highlighted:bg-destructive/20! relative w-auto min-w-36 animate-none! rounded-3xl p-1.5 shadow-lg ring-1 duration-100 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
Reference in New Issue
Block a user