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 ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import rehypeRaw from "rehype-raw";
|
|
||||||
import rehypeSanitize from "rehype-sanitize";
|
import rehypeSanitize from "rehype-sanitize";
|
||||||
|
|
||||||
interface MarkdownRendererProps {
|
interface MarkdownRendererProps {
|
||||||
@ -9,12 +8,74 @@ interface MarkdownRendererProps {
|
|||||||
className?: string;
|
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) {
|
export const MarkdownRenderer = memo(function MarkdownRenderer({ content, className }: MarkdownRendererProps) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<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
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
rehypePlugins={[rehypeSanitize]}
|
||||||
components={{
|
components={{
|
||||||
a: ({ href, children, ...props }) => (
|
a: ({ href, children, ...props }) => (
|
||||||
<a
|
<a
|
||||||
@ -30,6 +91,45 @@ export const MarkdownRenderer = memo(function MarkdownRenderer({ content, classN
|
|||||||
const safeAlt = alt || "";
|
const safeAlt = alt || "";
|
||||||
return <img src={src} alt={safeAlt} loading="lazy" {...props} />;
|
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}
|
{content}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||||
|
|
||||||
function Collapsible({
|
function Collapsible({
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||||
|
|
||||||
@ -51,15 +49,13 @@ function DialogContent({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
showOverlay = true,
|
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean
|
||||||
showOverlay?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
{showOverlay && <DialogOverlay />}
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Select as SelectPrimitive } from "radix-ui"
|
import { Select as SelectPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
106
src/index.css
106
src/index.css
@ -55,71 +55,71 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.13 0 0);
|
--foreground: oklch(0.145 0 0);
|
||||||
--card: oklch(1 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: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.13 0 0);
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--primary: oklch(0.13 0 0);
|
--primary: oklch(0.205 0 0);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.96 0 0);
|
--secondary: oklch(0.97 0 0);
|
||||||
--secondary-foreground: oklch(0.13 0 0);
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
--muted: oklch(0.96 0 0);
|
--muted: oklch(0.97 0 0);
|
||||||
--muted-foreground: oklch(0.45 0 0);
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
--accent: oklch(0.92 0 0);
|
--accent: oklch(0.97 0 0);
|
||||||
--accent-foreground: oklch(0.13 0 0);
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
--destructive: oklch(0.55 0.22 25);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.88 0 0);
|
--border: oklch(0.922 0 0);
|
||||||
--input: oklch(0.95 0 0);
|
--input: oklch(0.922 0 0);
|
||||||
--ring: oklch(0.50 0 0);
|
--ring: oklch(0.708 0 0);
|
||||||
--chart-1: oklch(0.45 0 0);
|
--chart-1: oklch(0.87 0 0);
|
||||||
--chart-2: oklch(0.35 0 0);
|
--chart-2: oklch(0.556 0 0);
|
||||||
--chart-3: oklch(0.25 0 0);
|
--chart-3: oklch(0.439 0 0);
|
||||||
--chart-4: oklch(0.18 0 0);
|
--chart-4: oklch(0.371 0 0);
|
||||||
--chart-5: oklch(0.12 0 0);
|
--chart-5: oklch(0.269 0 0);
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.98 0 0);
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: oklch(0.13 0 0);
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
--sidebar-primary: oklch(0.20 0 0);
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.95 0 0);
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.13 0 0);
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
--sidebar-border: oklch(0.90 0 0);
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
--sidebar-ring: oklch(0.50 0 0);
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.13 0 0);
|
--background: oklch(0.145 0 0);
|
||||||
--foreground: oklch(0.985 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);
|
--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);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.75 0 0);
|
--primary: oklch(0.922 0 0);
|
||||||
--primary-foreground: oklch(0.13 0 0);
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
--secondary: oklch(0.25 0 0);
|
--secondary: oklch(0.269 0 0);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.28 0 0);
|
--muted: oklch(0.269 0 0);
|
||||||
--muted-foreground: oklch(0.65 0 0);
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
--accent: oklch(0.25 0 0);
|
--accent: oklch(0.269 0 0);
|
||||||
--accent-foreground: oklch(0.20 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(0.90 0 0);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(0.92 0 0);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.55 0 0);
|
--ring: oklch(0.556 0 0);
|
||||||
--chart-1: oklch(0.50 0 0);
|
--chart-1: oklch(0.87 0 0);
|
||||||
--chart-2: oklch(0.40 0 0);
|
--chart-2: oklch(0.556 0 0);
|
||||||
--chart-3: oklch(0.30 0 0);
|
--chart-3: oklch(0.439 0 0);
|
||||||
--chart-4: oklch(0.20 0 0);
|
--chart-4: oklch(0.371 0 0);
|
||||||
--chart-5: oklch(0.10 0 0);
|
--chart-5: oklch(0.269 0 0);
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.205 0 0);
|
||||||
--sidebar-foreground: oklch(0.13 0 0);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.25 0 0);
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.20 0 0);
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(0.92 0 0);
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--sidebar-ring: oklch(0.55 0 0);
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────
|
/* ─────────────────────────────────────────────
|
||||||
@ -362,4 +362,4 @@
|
|||||||
--tw-prose-pre-bg: var(--surface-elevated);
|
--tw-prose-pre-bg: var(--surface-elevated);
|
||||||
--tw-prose-th-borders: var(--border-default);
|
--tw-prose-th-borders: var(--border-default);
|
||||||
--tw-prose-td-borders: var(--border-subtle);
|
--tw-prose-td-borders: var(--border-subtle);
|
||||||
}
|
}
|
||||||
@ -5,29 +5,23 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function truncate(text: string, maxLen: number): string {
|
||||||
* Strips basic markdown syntax from a string.
|
if (text.length <= maxLen) return text;
|
||||||
*/
|
return text.slice(0, maxLen).trimEnd() + "…";
|
||||||
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 stripMarkdown(text: string): string {
|
||||||
* Truncates a string to a maximum length and adds an ellipsis.
|
return text
|
||||||
*/
|
.replace(/#{1,6}\s+/g, "")
|
||||||
export function truncate(text: string, maxLength: number): string {
|
.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
||||||
if (!text || text.length <= maxLength) return text;
|
.replace(/(\*|_)(.*?)\1/g, "$2")
|
||||||
return text.substring(0, maxLength).trim() + "...";
|
.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