mirror of
https://github.com/codeaashu/claude-code.git
synced 2026-04-08 22:28:48 +03:00
claude-code
This commit is contained in:
170
web/components/adapted/Markdown.tsx
Normal file
170
web/components/adapted/Markdown.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Web-adapted Markdown renderer.
|
||||
*
|
||||
* The terminal Markdown (src/components/Markdown.tsx) uses marked + Ink's
|
||||
* <Ansi> / <Box> to render tokenised markdown as coloured ANSI output.
|
||||
* This web version uses react-markdown + remark-gfm + rehype-highlight, which
|
||||
* are already present in the web package, to render proper HTML with Tailwind
|
||||
* prose styles.
|
||||
*
|
||||
* Props are intentionally compatible with the terminal version so callers can
|
||||
* swap between them via the platform conditional.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface MarkdownProps {
|
||||
/** Markdown source string — matches the terminal component's children prop. */
|
||||
children: string;
|
||||
/** When true, render all text as visually dimmed (muted colour). */
|
||||
dimColor?: boolean;
|
||||
/** Extra class names applied to the prose wrapper. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ─── Inline code / pre renderers ─────────────────────────────────────────────
|
||||
|
||||
function InlineCode({ children }: { children?: React.ReactNode }) {
|
||||
return (
|
||||
<code className="px-1 py-0.5 rounded text-xs font-mono bg-surface-850 text-brand-300 border border-surface-700">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
|
||||
interface PreProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
function Pre({ children }: PreProps) {
|
||||
return (
|
||||
<pre className="overflow-x-auto rounded-md bg-surface-900 border border-surface-700 p-3 my-2 text-xs font-mono leading-relaxed">
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function Markdown({ children, dimColor = false, className }: MarkdownProps) {
|
||||
// Memoised to avoid re-parsing on every parent render.
|
||||
const content = useMemo(() => children, [children]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"markdown-body text-sm leading-relaxed font-mono",
|
||||
dimColor ? "text-surface-500" : "text-surface-100",
|
||||
|
||||
// Headings
|
||||
"[&_h1]:text-base [&_h1]:font-bold [&_h1]:mb-2 [&_h1]:mt-3 [&_h1]:text-surface-50",
|
||||
"[&_h2]:text-sm [&_h2]:font-semibold [&_h2]:mb-1.5 [&_h2]:mt-2.5 [&_h2]:text-surface-100",
|
||||
"[&_h3]:text-sm [&_h3]:font-semibold [&_h3]:mb-1 [&_h3]:mt-2 [&_h3]:text-surface-200",
|
||||
|
||||
// Paragraphs
|
||||
"[&_p]:my-1 [&_p]:leading-relaxed",
|
||||
|
||||
// Lists
|
||||
"[&_ul]:my-1 [&_ul]:pl-4 [&_ul]:list-disc",
|
||||
"[&_ol]:my-1 [&_ol]:pl-4 [&_ol]:list-decimal",
|
||||
"[&_li]:my-0.5",
|
||||
|
||||
// Blockquote
|
||||
"[&_blockquote]:border-l-2 [&_blockquote]:border-brand-500 [&_blockquote]:pl-3",
|
||||
"[&_blockquote]:my-2 [&_blockquote]:text-surface-400 [&_blockquote]:italic",
|
||||
|
||||
// Horizontal rule
|
||||
"[&_hr]:border-surface-700 [&_hr]:my-3",
|
||||
|
||||
// Tables (GFM)
|
||||
"[&_table]:w-full [&_table]:text-xs [&_table]:border-collapse [&_table]:my-2",
|
||||
"[&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_th]:border [&_th]:border-surface-700 [&_th]:bg-surface-800 [&_th]:font-semibold",
|
||||
"[&_td]:px-2 [&_td]:py-1 [&_td]:border [&_td]:border-surface-700",
|
||||
"[&_tr:nth-child(even)_td]:bg-surface-900/40",
|
||||
|
||||
// Links
|
||||
"[&_a]:text-brand-400 [&_a]:no-underline [&_a:hover]:underline",
|
||||
|
||||
// Strong / em
|
||||
"[&_strong]:font-bold [&_strong]:text-surface-50",
|
||||
"[&_em]:italic",
|
||||
|
||||
className
|
||||
)}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ className: cls, children: codeChildren, ...rest }) {
|
||||
const isBlock = /language-/.test(cls ?? "");
|
||||
if (isBlock) {
|
||||
return (
|
||||
<code className={cn("block text-surface-200", cls)} {...rest}>
|
||||
{codeChildren}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return <InlineCode {...rest}>{codeChildren}</InlineCode>;
|
||||
},
|
||||
pre: ({ children: preChildren }) => <Pre>{preChildren}</Pre>,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Table component (matches MarkdownTable.tsx surface) ─────────────────────
|
||||
|
||||
export interface MarkdownTableProps {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MarkdownTable({ headers, rows, className }: MarkdownTableProps) {
|
||||
return (
|
||||
<div className={cn("overflow-x-auto my-2", className)}>
|
||||
<table className="w-full text-xs border-collapse font-mono">
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((h, i) => (
|
||||
<th
|
||||
key={i}
|
||||
className="px-2 py-1 text-left border border-surface-700 bg-surface-800 font-semibold text-surface-200"
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, ri) => (
|
||||
<tr key={ri}>
|
||||
{row.map((cell, ci) => (
|
||||
<td
|
||||
key={ci}
|
||||
className={cn(
|
||||
"px-2 py-1 border border-surface-700 text-surface-300",
|
||||
ri % 2 === 1 && "bg-surface-900/40"
|
||||
)}
|
||||
>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
151
web/components/adapted/Spinner.tsx
Normal file
151
web/components/adapted/Spinner.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Web-adapted Spinner.
|
||||
*
|
||||
* The terminal Spinner (src/components/Spinner.tsx) drives animation via
|
||||
* useAnimationFrame and renders Unicode braille/block characters with ANSI
|
||||
* colour via Ink's <Text>. In the browser we replace that with a pure-CSS
|
||||
* spinning ring, preserving the same optional `tip` text and `mode` prop
|
||||
* surface so callers can swap in this component without changing props.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Mirrors the SpinnerMode type from src/components/Spinner/index.ts */
|
||||
export type SpinnerMode =
|
||||
| "queued"
|
||||
| "loading"
|
||||
| "thinking"
|
||||
| "auto"
|
||||
| "disabled";
|
||||
|
||||
export interface SpinnerProps {
|
||||
/** Visual mode — controls colour/appearance. */
|
||||
mode?: SpinnerMode;
|
||||
/** Optional tip text shown next to the spinner. */
|
||||
spinnerTip?: string;
|
||||
/** Override message replaces the default verb. */
|
||||
overrideMessage?: string | null;
|
||||
/** Additional suffix appended after the main label. */
|
||||
spinnerSuffix?: string | null;
|
||||
/** When true the spinner renders inline instead of as a block row. */
|
||||
inline?: boolean;
|
||||
/** Extra class names for the wrapper element. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ─── Colour map ───────────────────────────────────────────────────────────────
|
||||
|
||||
const MODE_RING_CLASS: Record<SpinnerMode, string> = {
|
||||
queued: "border-surface-500",
|
||||
loading: "border-brand-400",
|
||||
thinking: "border-brand-500",
|
||||
auto: "border-brand-400",
|
||||
disabled: "border-surface-600",
|
||||
};
|
||||
|
||||
const MODE_TEXT_CLASS: Record<SpinnerMode, string> = {
|
||||
queued: "text-surface-400",
|
||||
loading: "text-brand-300",
|
||||
thinking: "text-brand-300",
|
||||
auto: "text-brand-300",
|
||||
disabled: "text-surface-500",
|
||||
};
|
||||
|
||||
const MODE_LABEL: Record<SpinnerMode, string> = {
|
||||
queued: "Queued…",
|
||||
loading: "Loading…",
|
||||
thinking: "Thinking…",
|
||||
auto: "Working…",
|
||||
disabled: "",
|
||||
};
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function Spinner({
|
||||
mode = "loading",
|
||||
spinnerTip,
|
||||
overrideMessage,
|
||||
spinnerSuffix,
|
||||
inline = false,
|
||||
className,
|
||||
}: SpinnerProps) {
|
||||
if (mode === "disabled") return null;
|
||||
|
||||
const label =
|
||||
overrideMessage ??
|
||||
spinnerTip ??
|
||||
MODE_LABEL[mode];
|
||||
|
||||
const ringClass = MODE_RING_CLASS[mode];
|
||||
const textClass = MODE_TEXT_CLASS[mode];
|
||||
|
||||
return (
|
||||
<span
|
||||
role="status"
|
||||
aria-label={label || "Loading"}
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
inline ? "inline-flex" : "flex",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* CSS spinning ring */}
|
||||
<span
|
||||
className={cn(
|
||||
"block w-3.5 h-3.5 rounded-full border-2 border-transparent animate-spin flex-shrink-0",
|
||||
ringClass,
|
||||
// Top border only — creates the "gap" in the ring for the spinning effect
|
||||
"[border-top-color:currentColor]"
|
||||
)}
|
||||
style={{ borderTopColor: undefined }}
|
||||
aria-hidden
|
||||
>
|
||||
{/* Inner ring for the visible arc — achieved via box-shadow trick */}
|
||||
</span>
|
||||
|
||||
{(label || spinnerSuffix) && (
|
||||
<span className={cn("text-sm font-mono", textClass)}>
|
||||
{label}
|
||||
{spinnerSuffix && (
|
||||
<span className="text-surface-500 ml-1">{spinnerSuffix}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Shimmer / glimmer variant ────────────────────────────────────────────────
|
||||
|
||||
/** Pulsing shimmer bar — web replacement for GlimmerMessage / ShimmerChar. */
|
||||
export function ShimmerBar({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-2 rounded-full bg-gradient-to-r from-surface-700 via-surface-500 to-surface-700",
|
||||
"bg-[length:200%_100%] animate-shimmer",
|
||||
className
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline flashing cursor dot — web replacement for FlashingChar. */
|
||||
export function FlashingCursor({ className }: { className?: string }) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-block w-1.5 h-4 bg-current align-text-bottom ml-0.5",
|
||||
"animate-pulse-soft",
|
||||
className
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user