refactor(ui): update UI components, theme system and utilities
This commit is contained in:
parent
b384f92bbf
commit
e86803d235
@ -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}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||
|
||||
function Collapsible({
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
|
||||
@ -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
104
src/index.css
104
src/index.css
@ -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);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user