feat: PhotoPrism M0 bring-up — compose stack, web client, sidecar, migrate
Replace the legacy mule-image backend with PhotoPrism plus a thin SvelteKit client and a Node sidecar for endpoints PhotoPrism doesn't expose (file rename), and add a two-phase migrator (metadata via PUT, heaps → albums) for the existing Postgres library. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
web/src/lib/components/layout/KebabMenu.svelte
Normal file
56
web/src/lib/components/layout/KebabMenu.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<!--
|
||||
Thin wrapper around bits-ui's DM. Provides:
|
||||
- A round ⋯ trigger button styled like the rest of the sidebar's hover
|
||||
affordances (muted, becomes accent on hover/open).
|
||||
- A portal-positioned content container with shadcn-zinc styling.
|
||||
- An `Item` re-export consumers compose into the menu body so we don't
|
||||
also have to redeclare the item styling at every call site.
|
||||
|
||||
Items are passed as a snippet via `children` so callers can mix the
|
||||
`Item` re-export, separators, or destructive variants freely.
|
||||
-->
|
||||
<script lang="ts" module>
|
||||
import { DropdownMenu as DM } from 'bits-ui';
|
||||
export const Item = DM.Item;
|
||||
export const Separator = DM.Separator;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { MoreHorizontal } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
/** Tooltip + aria-label for the trigger button. */
|
||||
label?: string;
|
||||
/** Force the trigger visible regardless of hover state. Used when
|
||||
* the menu is open so it doesn't disappear underneath a row hover
|
||||
* transition while the user is interacting with it. */
|
||||
alwaysVisible?: boolean;
|
||||
children: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { label = 'More', alwaysVisible = false, children }: Props = $props();
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<DM.Root bind:open>
|
||||
<DM.Trigger
|
||||
class="rounded p-0.5 text-xs text-muted-foreground transition-opacity hover:bg-accent hover:text-foreground focus:outline-none {open ||
|
||||
alwaysVisible
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 group-hover:opacity-100'}"
|
||||
title={label}
|
||||
aria-label={label}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreHorizontal class="h-3.5 w-3.5" />
|
||||
</DM.Trigger>
|
||||
<DM.Portal>
|
||||
<DM.Content
|
||||
class="z-50 min-w-[180px] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-none"
|
||||
sideOffset={4}
|
||||
align="end"
|
||||
>
|
||||
{@render children()}
|
||||
</DM.Content>
|
||||
</DM.Portal>
|
||||
</DM.Root>
|
||||
Reference in New Issue
Block a user