feat(ui,api,telegram): shared conversation panel for obsidian and /admin/panel, bind materializes a window, inbox survives link previews
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import Link2Icon from "@lucide/svelte/icons/link-2";
|
||||
import Link2OffIcon from "@lucide/svelte/icons/link-2-off";
|
||||
import type { ApiClient } from "$lib/api/client";
|
||||
import KindBadge from "$lib/components/kind-badge.svelte";
|
||||
import LiveDot from "$lib/components/live-dot.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import { clip, shortId } from "$lib/format";
|
||||
import ConversationPicker from "./conversation-picker.svelte";
|
||||
import type { ConversationIndex } from "./index.svelte";
|
||||
|
||||
let {
|
||||
client,
|
||||
index,
|
||||
selected = null,
|
||||
onOpen,
|
||||
follow = $bindable(false),
|
||||
showFollow = false,
|
||||
}: {
|
||||
client: ApiClient;
|
||||
index: ConversationIndex;
|
||||
selected?: string | null;
|
||||
onOpen: (id: string) => void;
|
||||
follow?: boolean;
|
||||
showFollow?: boolean;
|
||||
} = $props();
|
||||
|
||||
const TITLE_MAX = 48;
|
||||
let switcherOpen = $state(false);
|
||||
const current = $derived(selected ? index.byId(selected) : undefined);
|
||||
const running = $derived(index.running.length);
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-1 border-b px-2 py-1.5">
|
||||
<Popover.Root bind:open={switcherOpen}>
|
||||
<Popover.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<button
|
||||
{...props}
|
||||
aria-label="Switch conversation"
|
||||
class="row-hover flex h-8 min-w-0 flex-1 items-center gap-2 rounded-md px-2 text-left text-sm aria-expanded:bg-muted"
|
||||
type="button"
|
||||
>
|
||||
{#if current}
|
||||
<KindBadge kind={current.kind} />
|
||||
<span class="min-w-0 flex-1 truncate font-medium">
|
||||
{current.title
|
||||
? clip(current.title, TITLE_MAX)
|
||||
: `${current.kind} · ${shortId(current.id)}`}
|
||||
</span>
|
||||
{:else if selected}
|
||||
<span class="tabular min-w-0 flex-1 truncate font-medium">
|
||||
{shortId(selected)}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="min-w-0 flex-1 truncate text-muted-foreground">
|
||||
Pick a conversation
|
||||
</span>
|
||||
{/if}
|
||||
{#if running > 0 && !current?.running_turn}
|
||||
<span
|
||||
class="tabular shrink-0 rounded-full bg-signal/12 px-1.5 font-medium text-signal text-xs"
|
||||
title="{running} running elsewhere"
|
||||
>
|
||||
{running}
|
||||
</span>
|
||||
{/if}
|
||||
<ChevronDownIcon class="size-3.5 shrink-0 text-icon" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
align="start"
|
||||
class="w-[min(24rem,calc(100vw-1rem))] gap-0 overflow-hidden p-0"
|
||||
sideOffset={6}
|
||||
>
|
||||
<div class="flex max-h-[min(70vh,32rem)] flex-col">
|
||||
<ConversationPicker
|
||||
autofocus
|
||||
{client}
|
||||
{index}
|
||||
onOpen={(id) => {
|
||||
switcherOpen = false;
|
||||
onOpen(id);
|
||||
}}
|
||||
{selected}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
<LiveDot
|
||||
class="px-1"
|
||||
detail={index.live.detail}
|
||||
label={false}
|
||||
state={index.live.state}
|
||||
/>
|
||||
{#if showFollow}
|
||||
<Button
|
||||
aria-label="Follow the active note"
|
||||
aria-pressed={follow}
|
||||
onclick={() => {
|
||||
follow = !follow;
|
||||
}}
|
||||
size="icon-sm"
|
||||
title={follow
|
||||
? "Following the active note - click to pin this conversation"
|
||||
: "Pinned - click to follow the active note"}
|
||||
variant={follow ? "secondary" : "ghost"}
|
||||
>
|
||||
{#if follow}
|
||||
<Link2Icon class="size-4" />
|
||||
{:else}
|
||||
<Link2OffIcon class="size-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user