feat(ui,api): strip board redesign - island, rail by day, context view, server search, vault graph

This commit is contained in:
hh committed 2026-09-02 03:48:26 +02:00
1 parent 85b14e2c2f
commit 025f2dbc3f
60 files changed
+5071 -2347

No files matched your search

+64 -58
View File
@@ -1,9 +1,8 @@
<script lang="ts">
import SendHorizontalIcon from "@lucide/svelte/icons/send-horizontal";
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
import { toast } from "svelte-sonner";
import type { ApiClient } from "$lib/api/client";
import { Button } from "$lib/components/ui/button";
import * as Select from "$lib/components/ui/select";
import { cn } from "$lib/utils";
let {
client,
@@ -17,28 +16,31 @@
type Mode = "message" | "inject" | "urgent";
const MODES: { value: Mode; label: string; hint: string }[] = [
{ hint: "as you, mirrored to the window", label: "Say", value: "message" },
{
hint: "as the user, mirrored to Telegram",
label: "Message",
value: "message",
},
{
hint: "system note, rides with the next turn",
hint: "a system note for the next turn",
label: "Inject",
value: "inject",
},
{
hint: "interrupts the running turn",
label: "Urgent inject",
value: "urgent",
},
{ hint: "interrupts the running turn", label: "Urgent", value: "urgent" },
];
const MAX_ROWS = 10;
const LINE_PX = 22;
let mode = $state<Mode>("message");
let text = $state("");
let busy = $state(false);
let area = $state<HTMLTextAreaElement | null>(null);
const current = $derived(MODES.find((m) => m.value === mode) ?? MODES[0]);
function grow() {
if (!area) {
return;
}
area.style.height = "auto";
area.style.height = `${Math.min(area.scrollHeight, LINE_PX * MAX_ROWS)}px`;
}
async function send() {
const body = text.trim();
if (!body || busy) {
@@ -56,6 +58,7 @@
);
}
text = "";
queueMicrotask(grow);
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error));
} finally {
@@ -72,57 +75,60 @@
</script>
<form
class="flex flex-col gap-2 border-t bg-background px-3 py-2"
class="flex flex-col gap-1.5 px-3 pt-2 pb-[max(0.5rem,var(--beaver-inset-bottom,0px))] @md:px-6"
onsubmit={(event) => {
event.preventDefault();
send();
}}
>
<textarea
aria-label="Message"
class="min-h-16 w-full resize-y rounded-md border bg-input/30 px-3 py-2 text-sm focus-visible:border-ring focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/30"
{disabled}
onkeydown={onKeydown}
placeholder={mode === "message"
? "Say something to the agent…"
: "Text of the inject…"}
rows="2"
bind:value={text}
></textarea>
<div class="flex items-center gap-2">
<Select.Root
onValueChange={(value) => {
mode = value as Mode;
}}
type="single"
value={mode}
>
<Select.Trigger class="h-8 text-xs" size="sm"
>{current.label}</Select.Trigger
>
<Select.Content>
{#each MODES as option (option.value)}
<Select.Item label={option.label} value={option.value}>
<span class="flex flex-col">
<span>{option.label}</span>
<span class="text-muted-foreground text-xs">{option.hint}</span>
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<span class="hidden text-muted-foreground text-xs @md:inline"
>{current.hint}</span
>
<Button
class="ml-auto"
<div
class={cn(
"flex items-end gap-2 rounded-xl bg-strip px-3 py-2 shadow-lift ring-1 ring-border focus-within:ring-ring",
disabled && "opacity-60"
)}
>
<textarea
aria-label="Message"
class="max-h-56 min-h-6 w-full resize-none bg-transparent py-0.5 text-sm leading-[22px] outline-none placeholder:text-muted-foreground"
{disabled}
oninput={grow}
onkeydown={onKeydown}
placeholder={disabled
? "This conversation is closed."
: `${current.label} something… (⌘↩)`}
rows="1"
bind:this={area}
bind:value={text}
></textarea>
<button
aria-label="Send"
class="inline-flex size-7 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground transition-opacity disabled:opacity-30"
disabled={disabled || busy || !text.trim()}
size="sm"
type="submit"
>
<SendHorizontalIcon class="size-4" />
Send
<kbd class="hidden text-[10px] opacity-70 @md:inline">⌘↩</kbd>
</Button>
<ArrowUpIcon class="size-4" />
</button>
</div>
<div class="flex items-center gap-3 px-1">
{#each MODES as option (option.value)}
<button
class={cn(
"rule-word text-xs",
mode === option.value && "text-foreground",
option.value === "urgent" && mode === option.value && "text-destructive"
)}
data-active={mode === option.value}
onclick={() => {
mode = option.value;
}}
title={option.hint}
type="button"
>
{option.label}
</button>
{/each}
<span class="hidden truncate text-muted-foreground text-xs @md:inline">
{current.hint}
</span>
</div>
</form>