ui: migrate ChatMessage to shadcn tokens

Use shadcn Button/Textarea, @lucide/svelte icons, and map prose/code-block
styles to shadcn CSS variables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmuGqwNBJNEAMaWsdCDqUC
This commit is contained in:
Claude
2026-06-27 11:58:24 +00:00
parent 746c083a87
commit 2b087dffb1
@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { renderMarkdown, escapeHtml, renderStreamingMarkdown, createStreamingCache } from "../../lib/markdown"; import { renderMarkdown, escapeHtml, renderStreamingMarkdown, createStreamingCache } from "../../lib/markdown";
import type { RenderedBlock } from "../../lib/markdown"; import type { RenderedBlock } from "../../lib/markdown";
import { Copy, Check, Pencil, X, Save, RefreshCw, ChevronDown, ChevronRight, Brain, Code } from "lucide-svelte"; import { Copy, Check, Pencil, X, Save, RefreshCw, ChevronDown, ChevronRight, Brain, Code } from "@lucide/svelte";
import { Button } from "$lib/components/ui/button/index.js";
import { Textarea } from "$lib/components/ui/textarea/index.js";
import { getTextContent, getImageUrls } from "../../lib/types"; import { getTextContent, getImageUrls } from "../../lib/types";
import type { ContentPart } from "../../lib/types"; import type { ContentPart } from "../../lib/types";
@@ -161,37 +163,37 @@
<div class="flex {role === 'user' ? 'justify-end' : 'justify-start'} mb-4"> <div class="flex {role === 'user' ? 'justify-end' : 'justify-start'} mb-4">
<div <div
class="relative group rounded-lg px-4 py-2 {role === 'user' class="group relative rounded-lg px-4 py-2 {role === 'user'
? 'max-w-[85%] bg-primary text-btn-primary-text' ? 'bg-primary text-primary-foreground max-w-[85%]'
: 'w-full sm:w-4/5 bg-surface border border-gray-200 dark:border-white/10'}" : 'bg-card w-full border sm:w-4/5'}"
> >
{#if role === "assistant"} {#if role === "assistant"}
{#if reasoning_content || isReasoning} {#if reasoning_content || isReasoning}
<div class="mb-3 border border-gray-200 dark:border-white/10 rounded overflow-hidden"> <div class="mb-3 overflow-hidden rounded border">
<button <button
class="w-full flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-white/5 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors text-sm" class="bg-muted/50 hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors"
onclick={() => showReasoning = !showReasoning} onclick={() => showReasoning = !showReasoning}
> >
{#if showReasoning} {#if showReasoning}
<ChevronDown class="w-4 h-4" /> <ChevronDown class="size-4" />
{:else} {:else}
<ChevronRight class="w-4 h-4" /> <ChevronRight class="size-4" />
{/if} {/if}
<Brain class="w-4 h-4" /> <Brain class="size-4" />
<span class="font-medium">Reasoning</span> <span class="font-medium">Reasoning</span>
<span class="text-txtsecondary ml-2"> <span class="text-muted-foreground ml-2">
({reasoning_content.length} chars{#if !isReasoning && reasoningTimeMs > 0}, {formatDuration(reasoningTimeMs)}{/if}) ({reasoning_content.length} chars{#if !isReasoning && reasoningTimeMs > 0}, {formatDuration(reasoningTimeMs)}{/if})
</span> </span>
{#if isReasoning} {#if isReasoning}
<span class="ml-auto flex items-center gap-1 text-txtsecondary"> <span class="text-muted-foreground ml-auto flex items-center gap-1">
<span class="w-1.5 h-1.5 bg-primary rounded-full animate-pulse"></span> <span class="bg-primary h-1.5 w-1.5 animate-pulse rounded-full"></span>
reasoning... reasoning...
</span> </span>
{/if} {/if}
</button> </button>
{#if showReasoning} {#if showReasoning}
<div class="px-3 py-2 bg-gray-50/50 dark:bg-white/[0.02] text-sm text-txtsecondary whitespace-pre-wrap font-mono"> <div class="bg-muted/30 text-muted-foreground whitespace-pre-wrap px-3 py-2 font-mono text-sm">
{reasoning_content}{#if isReasoning}<span class="inline-block w-1.5 h-4 bg-current animate-pulse ml-0.5"></span>{/if} {reasoning_content}{#if isReasoning}<span class="ml-0.5 inline-block h-4 w-1.5 animate-pulse bg-current"></span>{/if}
</div> </div>
{/if} {/if}
</div> </div>
@@ -201,7 +203,7 @@
{#each imageUrls as imageUrl, idx (idx)} {#each imageUrls as imageUrl, idx (idx)}
<button <button
onclick={() => openModal(imageUrl)} onclick={() => openModal(imageUrl)}
class="cursor-pointer rounded border border-gray-200 dark:border-white/10 hover:opacity-80 transition-opacity" class="cursor-pointer rounded border transition-opacity hover:opacity-80"
> >
<img <img
src={imageUrl} src={imageUrl}
@@ -226,60 +228,47 @@
</div> </div>
{/if} {/if}
{#if !isStreaming} {#if !isStreaming}
<div class="flex gap-1 mt-2 pt-1 border-t border-gray-200 dark:border-white/10"> <div class="mt-2 flex gap-1 border-t pt-1">
{#if onRegenerate} {#if onRegenerate}
<button <Button variant="ghost" size="icon-xs" class="text-muted-foreground" onclick={onRegenerate} title="Regenerate response">
class="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 text-txtsecondary" <RefreshCw />
onclick={onRegenerate} </Button>
title="Regenerate response"
>
<RefreshCw class="w-4 h-4" />
</button>
{/if} {/if}
<button <Button
class="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 text-txtsecondary" variant="ghost"
size="icon-xs"
class="text-muted-foreground"
onclick={copyToClipboard} onclick={copyToClipboard}
title={copied ? "Copied!" : "Copy to clipboard"} title={copied ? "Copied!" : "Copy to clipboard"}
> >
{#if copied} {#if copied}
<Check class="w-4 h-4 text-green-500" /> <Check class="text-success" />
{:else} {:else}
<Copy class="w-4 h-4" /> <Copy />
{/if} {/if}
</button> </Button>
<button <Button
class="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 {showRaw ? 'text-primary' : 'text-txtsecondary'}" variant="ghost"
size="icon-xs"
class={showRaw ? "text-primary" : "text-muted-foreground"}
onclick={() => showRaw = !showRaw} onclick={() => showRaw = !showRaw}
title={showRaw ? "Show rendered" : "Show raw"} title={showRaw ? "Show rendered" : "Show raw"}
> >
<Code class="w-4 h-4" /> <Code />
</button> </Button>
</div> </div>
{/if} {/if}
{:else} {:else}
{#if isEditing} {#if isEditing}
<div class="flex flex-col gap-2 min-w-[300px]"> <div class="flex min-w-[300px] flex-col gap-2">
<textarea <Textarea class="resize-none" rows={3} bind:value={editContent} onkeydown={handleKeyDown} />
class="w-full px-3 py-2 rounded border border-gray-200 dark:border-white/10 bg-surface text-txtmain focus:outline-none focus:ring-2 focus:ring-primary resize-none"
rows="3"
bind:value={editContent}
onkeydown={handleKeyDown}
></textarea>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button <Button variant="ghost" size="icon-sm" onclick={cancelEdit} title="Cancel">
class="p-1.5 rounded hover:bg-white/20" <X />
onclick={cancelEdit} </Button>
title="Cancel" <Button variant="ghost" size="icon-sm" onclick={saveEdit} title="Save">
> <Save />
<X class="w-4 h-4" /> </Button>
</button>
<button
class="p-1.5 rounded hover:bg-white/20"
onclick={saveEdit}
title="Save"
>
<Save class="w-4 h-4" />
</button>
</div> </div>
</div> </div>
{:else} {:else}
@@ -288,7 +277,7 @@
{#each imageUrls as imageUrl, idx (idx)} {#each imageUrls as imageUrl, idx (idx)}
<button <button
onclick={() => openModal(imageUrl)} onclick={() => openModal(imageUrl)}
class="cursor-pointer rounded border border-white/20 hover:opacity-80 transition-opacity" class="cursor-pointer rounded border border-white/20 transition-opacity hover:opacity-80"
> >
<img <img
src={imageUrl} src={imageUrl}
@@ -302,11 +291,11 @@
<div class="whitespace-pre-wrap pr-8">{textContent}</div> <div class="whitespace-pre-wrap pr-8">{textContent}</div>
{#if canEdit} {#if canEdit}
<button <button
class="absolute top-2 right-2 p-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity bg-white/20 hover:bg-white/30 shadow-sm" class="absolute right-2 top-2 rounded-lg bg-white/20 p-1.5 opacity-0 shadow-sm transition-opacity hover:bg-white/30 group-hover:opacity-100"
onclick={startEdit} onclick={startEdit}
title="Edit message" title="Edit message"
> >
<Pencil class="w-4 h-4" /> <Pencil class="size-4" />
</button> </button>
{/if} {/if}
{/if} {/if}
@@ -324,11 +313,11 @@
tabindex="-1" tabindex="-1"
> >
<button <button
class="absolute top-4 right-4 p-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors" class="absolute right-4 top-4 rounded-lg bg-white/10 p-2 text-white transition-colors hover:bg-white/20"
onclick={() => closeModal()} onclick={() => closeModal()}
title="Close" title="Close"
> >
<X class="w-6 h-6" /> <X class="size-6" />
</button> </button>
<img <img
src={modalImageUrl} src={modalImageUrl}
@@ -341,8 +330,8 @@
<style> <style>
.prose :global(pre) { .prose :global(pre) {
position: relative; position: relative;
background-color: var(--color-surface); background-color: var(--muted);
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.2)); border: 1px solid var(--border);
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 0.75rem; padding: 0.75rem;
padding-right: 2.5rem; padding-right: 2.5rem;
@@ -359,20 +348,20 @@
justify-content: center; justify-content: center;
padding: 0.25rem; padding: 0.25rem;
border-radius: 0.25rem; border-radius: 0.25rem;
border: 1px solid var(--color-border); border: 1px solid var(--border);
background: var(--color-surface); background: var(--muted);
color: var(--color-txtsecondary); color: var(--muted-foreground);
cursor: pointer; cursor: pointer;
transition: background-color 0.15s; transition: background-color 0.15s;
line-height: 0; line-height: 0;
} }
.prose :global(.code-copy-btn:hover) { .prose :global(.code-copy-btn:hover) {
background: var(--color-secondary); background: var(--accent);
} }
.prose :global(.code-copy-btn.copied) { .prose :global(.code-copy-btn.copied) {
color: var(--color-success); color: var(--success);
opacity: 1; opacity: 1;
} }
@@ -387,10 +376,10 @@
} }
.prose :global(code:not(pre code)) { .prose :global(code:not(pre code)) {
background-color: var(--color-surface); background-color: var(--muted);
padding: 0.125rem 0.25rem; padding: 0.125rem 0.25rem;
border-radius: 0.25rem; border-radius: 0.25rem;
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.2)); border: 1px solid var(--border);
} }
.prose :global(p) { .prose :global(p) {
@@ -431,14 +420,14 @@
} }
.prose :global(blockquote) { .prose :global(blockquote) {
border-left: 3px solid var(--color-primary); border-left: 3px solid var(--primary);
padding-left: 1rem; padding-left: 1rem;
margin: 0.5rem 0; margin: 0.5rem 0;
font-style: italic; font-style: italic;
} }
.prose :global(a) { .prose :global(a) {
color: var(--color-primary); color: var(--primary);
text-decoration: underline; text-decoration: underline;
} }
@@ -450,13 +439,13 @@
.prose :global(th), .prose :global(th),
.prose :global(td) { .prose :global(td) {
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.2)); border: 1px solid var(--border);
padding: 0.5rem; padding: 0.5rem;
text-align: left; text-align: left;
} }
.prose :global(th) { .prose :global(th) {
background-color: var(--color-surface); background-color: var(--muted);
font-weight: 600; font-weight: 600;
} }