refactor(ui): update UI components, theme system and utilities

This commit is contained in:
ZhenYi 2026-05-12 13:05:28 +08:00
parent b384f92bbf
commit e86803d235
10 changed files with 1033 additions and 809 deletions

View File

@ -1,7 +1,6 @@
import { memo } from "react";
import { memo, useRef, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
interface MarkdownRendererProps {
@ -9,12 +8,74 @@ interface MarkdownRendererProps {
className?: string;
}
/** Sanitize raw HTML: strip <script> and event handlers. */
function sanitizeHtml(raw: string): string {
return raw
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
}
/** Render raw HTML inside a Shadow DOM to scope CSS to this block only. */
function HtmlBlock({ html }: { html: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const shadow = el.shadowRoot || el.attachShadow({ mode: "open" });
shadow.innerHTML = sanitizeHtml(html);
}, [html]);
return <div ref={ref} className="my-2" />;
}
export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
return (
<div className={className}>
<style>{`
.markdown-table-wrapper table {
border-collapse: collapse;
width: 100%;
font-size: 14px;
}
.markdown-table-wrapper th,
.markdown-table-wrapper td {
border: 1px solid var(--border-default);
padding: 8px 12px;
text-align: left;
}
.markdown-table-wrapper th {
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
background-color: var(--surface-elevated);
}
.markdown-table-wrapper td {
color: var(--text-secondary);
}
.markdown-table-wrapper tr {
border-bottom: 1px solid var(--border-subtle);
}
.markdown-code-block {
background-color: var(--surface-elevated) !important;
border: 1px solid var(--border-default);
border-radius: 10px;
padding: 16px;
overflow-x: auto;
font-size: 13px;
line-height: 1.6;
}
.markdown-code-block code {
background: none !important;
border: none !important;
padding: 0 !important;
font-size: inherit;
color: var(--text-primary);
}
`}</style>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
rehypePlugins={[rehypeSanitize]}
components={{
a: ({ href, children, ...props }) => (
<a
@ -30,6 +91,45 @@ export const MarkdownRenderer = memo(function MarkdownRenderer({ content, classN
const safeAlt = alt || "";
return <img src={src} alt={safeAlt} loading="lazy" {...props} />;
},
table: ({ children }) => (
<div className="overflow-x-auto my-2 markdown-table-wrapper">
<table>{children}</table>
</div>
),
code: ({ children, className, ...props }) => {
const cls = Array.isArray(className) ? className.join(" ") : (className || "");
const match = /language-(\w+)/.exec(cls);
const isInline = !match;
// ````html` blocks: render inside Shadow DOM to scope CSS
if (match?.[1] === "html") {
const raw = typeof children === "string" ? children : "";
return <HtmlBlock html={raw} />;
}
if (isInline) {
return (
<code
className="px-1.5 py-0.5 rounded text-[13px]"
style={{
backgroundColor: "var(--surface-elevated)",
border: "0.5px solid var(--border-subtle)",
color: "var(--text-primary)",
}}
{...props}
>
{children}
</code>
);
}
return (
<pre className="markdown-code-block">
<code className={className} {...props}>
{children}
</code>
</pre>
);
},
}}
>
{content}

View File

@ -1,5 +1,3 @@
"use client"
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
function Collapsible({

View File

@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "radix-ui"
@ -51,15 +49,13 @@ function DialogContent({
className,
children,
showCloseButton = true,
showOverlay = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
showOverlay?: boolean;
showCloseButton?: boolean
}) {
return (
<DialogPortal>
{showOverlay && <DialogOverlay />}
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(

View File

@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"

View File

@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import { HoverCard as HoverCardPrimitive } from "radix-ui"

View File

@ -1,3 +1,5 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

View File

@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"

File diff suppressed because it is too large Load Diff

View File

@ -55,71 +55,71 @@
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.13 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.13 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.13 0 0);
--primary: oklch(0.13 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.96 0 0);
--secondary-foreground: oklch(0.13 0 0);
--muted: oklch(0.96 0 0);
--muted-foreground: oklch(0.45 0 0);
--accent: oklch(0.92 0 0);
--accent-foreground: oklch(0.13 0 0);
--destructive: oklch(0.55 0.22 25);
--border: oklch(0.88 0 0);
--input: oklch(0.95 0 0);
--ring: oklch(0.50 0 0);
--chart-1: oklch(0.45 0 0);
--chart-2: oklch(0.35 0 0);
--chart-3: oklch(0.25 0 0);
--chart-4: oklch(0.18 0 0);
--chart-5: oklch(0.12 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.98 0 0);
--sidebar-foreground: oklch(0.13 0 0);
--sidebar-primary: oklch(0.20 0 0);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.95 0 0);
--sidebar-accent-foreground: oklch(0.13 0 0);
--sidebar-border: oklch(0.90 0 0);
--sidebar-ring: oklch(0.50 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.13 0 0);
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.18 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.18 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.75 0 0);
--primary-foreground: oklch(0.13 0 0);
--secondary: oklch(0.25 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.28 0 0);
--muted-foreground: oklch(0.65 0 0);
--accent: oklch(0.25 0 0);
--accent-foreground: oklch(0.20 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.90 0 0);
--input: oklch(0.92 0 0);
--ring: oklch(0.55 0 0);
--chart-1: oklch(0.50 0 0);
--chart-2: oklch(0.40 0 0);
--chart-3: oklch(0.30 0 0);
--chart-4: oklch(0.20 0 0);
--chart-5: oklch(0.10 0 0);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.13 0 0);
--sidebar-primary: oklch(0.25 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.20 0 0);
--sidebar-border: oklch(0.92 0 0);
--sidebar-ring: oklch(0.55 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
/*
@ -362,4 +362,4 @@
--tw-prose-pre-bg: var(--surface-elevated);
--tw-prose-th-borders: var(--border-default);
--tw-prose-td-borders: var(--border-subtle);
}
}

View File

@ -5,29 +5,23 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Strips basic markdown syntax from a string.
*/
export function stripMarkdown(text: string): string {
if (!text) return "";
return text
.replace(/^#+\s+/gm, "") // Headings
.replace(/(\*\*|__)(.*?)\1/g, "$2") // Bold
.replace(/(\*|_)(.*?)\1/g, "$2") // Italic
.replace(/\[(.*?)\]\(.*?\)/g, "$1") // Links
.replace(/`{1,3}(.*?)`{1,3}/g, "$1") // Code
.replace(/^[*-]\s+/gm, "") // List items
.replace(/^\s*>\s+/gm, "") // Blockquotes
.replace(/\|.*?\|/g, "") // Table cells
.replace(/[-:]{3,}/g, "") // Table dividers
.replace(/\n+/g, " ") // Newlines to spaces
.trim();
export function truncate(text: string, maxLen: number): string {
if (text.length <= maxLen) return text;
return text.slice(0, maxLen).trimEnd() + "…";
}
/**
* Truncates a string to a maximum length and adds an ellipsis.
*/
export function truncate(text: string, maxLength: number): string {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength).trim() + "...";
export function stripMarkdown(text: string): string {
return text
.replace(/#{1,6}\s+/g, "")
.replace(/(\*\*|__)(.*?)\1/g, "$2")
.replace(/(\*|_)(.*?)\1/g, "$2")
.replace(/~~(.*?)~~/g, "$1")
.replace(/`{1,3}[^`]*`{1,3}/g, "")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/!\[[^\]]*\]\([^)]+\)/g, "")
.replace(/>\s+/g, "")
.replace(/^[-*+]\s+/gm, "")
.replace(/^\d+\.\s+/gm, "")
.replace(/\n{2,}/g, " ")
.trim();
}