From 488d67389dea53bbb8e50568391848cecd7d7544 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:39:32 +0000 Subject: [PATCH 1/7] Initial plan From 1cd46534b6c4c1daae37f3ab2fecbd84164669c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:43:44 +0000 Subject: [PATCH 2/7] Add base Chatbot UI component and renderer Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- .../src/renderers/complex/chatbot.test.ts | 39 +++ .../src/renderers/complex/chatbot.tsx | 167 ++++++++++++ .../components/src/renderers/complex/index.ts | 1 + packages/components/src/ui/chatbot.tsx | 240 ++++++++++++++++++ packages/components/src/ui/index.ts | 1 + 5 files changed, 448 insertions(+) create mode 100644 packages/components/src/renderers/complex/chatbot.test.ts create mode 100644 packages/components/src/renderers/complex/chatbot.tsx create mode 100644 packages/components/src/ui/chatbot.tsx diff --git a/packages/components/src/renderers/complex/chatbot.test.ts b/packages/components/src/renderers/complex/chatbot.test.ts new file mode 100644 index 000000000..607b54568 --- /dev/null +++ b/packages/components/src/renderers/complex/chatbot.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest'; +import { ComponentRegistry } from '@object-ui/core'; + +describe('Chatbot Component', () => { + it('should be registered in ComponentRegistry', () => { + const chatbotRenderer = ComponentRegistry.get('chatbot'); + expect(chatbotRenderer).toBeDefined(); + }); + + it('should have proper metadata', () => { + const metadata = ComponentRegistry.getMetadata('chatbot'); + expect(metadata).toBeDefined(); + expect(metadata?.label).toBe('Chatbot'); + expect(metadata?.inputs).toBeDefined(); + expect(metadata?.defaultProps).toBeDefined(); + }); + + it('should have expected inputs', () => { + const metadata = ComponentRegistry.getMetadata('chatbot'); + const inputNames = metadata?.inputs?.map((input: any) => input.name) || []; + + expect(inputNames).toContain('messages'); + expect(inputNames).toContain('placeholder'); + expect(inputNames).toContain('showTimestamp'); + expect(inputNames).toContain('userAvatar'); + expect(inputNames).toContain('assistantAvatar'); + }); + + it('should have sensible default props', () => { + const metadata = ComponentRegistry.getMetadata('chatbot'); + const defaults = metadata?.defaultProps; + + expect(defaults).toBeDefined(); + expect(defaults?.placeholder).toBe('Type your message...'); + expect(defaults?.showTimestamp).toBe(false); + expect(defaults?.messages).toBeDefined(); + expect(Array.isArray(defaults?.messages)).toBe(true); + }); +}); diff --git a/packages/components/src/renderers/complex/chatbot.tsx b/packages/components/src/renderers/complex/chatbot.tsx new file mode 100644 index 000000000..404c035d8 --- /dev/null +++ b/packages/components/src/renderers/complex/chatbot.tsx @@ -0,0 +1,167 @@ +import { ComponentRegistry } from '@object-ui/core'; +import { Chatbot, ChatbotMessage } from '@/ui'; +import { useState } from 'react'; + +ComponentRegistry.register('chatbot', + ({ schema, className, ...props }) => { + // Initialize messages from schema or use empty array + const [messages, setMessages] = useState( + schema.messages?.map((msg: any, idx: number) => ({ + id: msg.id || `msg-${idx}`, + role: msg.role || 'user', + content: msg.content || '', + timestamp: msg.timestamp, + avatar: msg.avatar, + avatarFallback: msg.avatarFallback, + })) || [] + ); + + // Handle sending new messages + const handleSendMessage = (content: string) => { + // Create user message + const userMessage: ChatbotMessage = { + id: `msg-${Date.now()}`, + role: 'user', + content, + timestamp: schema.showTimestamp ? new Date().toLocaleTimeString() : undefined, + }; + + setMessages((prev) => [...prev, userMessage]); + + // If onSend callback is provided in schema, call it + if (schema.onSend) { + schema.onSend(content, messages); + } + + // Auto-response feature for demo purposes + if (schema.autoResponse) { + setTimeout(() => { + const assistantMessage: ChatbotMessage = { + id: `msg-${Date.now()}-response`, + role: 'assistant', + content: schema.autoResponseText || 'Thank you for your message!', + timestamp: schema.showTimestamp ? new Date().toLocaleTimeString() : undefined, + }; + setMessages((prev) => [...prev, assistantMessage]); + }, schema.autoResponseDelay || 1000); + } + }; + + return ( + + ); + }, + { + label: 'Chatbot', + inputs: [ + { + name: 'messages', + type: 'array', + label: 'Initial Messages', + description: 'Array of message objects with id, role, content, and optional timestamp' + }, + { + name: 'placeholder', + type: 'string', + label: 'Input Placeholder', + defaultValue: 'Type your message...' + }, + { + name: 'showTimestamp', + type: 'boolean', + label: 'Show Timestamps', + defaultValue: false + }, + { + name: 'disabled', + type: 'boolean', + label: 'Disabled', + defaultValue: false + }, + { + name: 'userAvatar', + type: 'string', + label: 'User Avatar URL' + }, + { + name: 'userAvatarFallback', + type: 'string', + label: 'User Avatar Fallback', + defaultValue: 'You' + }, + { + name: 'assistantAvatar', + type: 'string', + label: 'Assistant Avatar URL' + }, + { + name: 'assistantAvatarFallback', + type: 'string', + label: 'Assistant Avatar Fallback', + defaultValue: 'AI' + }, + { + name: 'maxHeight', + type: 'string', + label: 'Max Height', + defaultValue: '500px' + }, + { + name: 'autoResponse', + type: 'boolean', + label: 'Enable Auto Response (Demo)', + defaultValue: false, + description: 'Automatically send a response after user message (for demo purposes)' + }, + { + name: 'autoResponseText', + type: 'string', + label: 'Auto Response Text', + defaultValue: 'Thank you for your message!' + }, + { + name: 'autoResponseDelay', + type: 'number', + label: 'Auto Response Delay (ms)', + defaultValue: 1000 + }, + { + name: 'className', + type: 'string', + label: 'CSS Class' + } + ], + defaultProps: { + messages: [ + { + id: 'welcome', + role: 'assistant', + content: 'Hello! How can I help you today?', + } + ], + placeholder: 'Type your message...', + showTimestamp: false, + disabled: false, + userAvatarFallback: 'You', + assistantAvatarFallback: 'AI', + maxHeight: '500px', + autoResponse: true, + autoResponseText: 'Thank you for your message! This is an automated response.', + autoResponseDelay: 1000, + className: 'w-full max-w-2xl' + } + } +); diff --git a/packages/components/src/renderers/complex/index.ts b/packages/components/src/renderers/complex/index.ts index 4957d296e..599499728 100644 --- a/packages/components/src/renderers/complex/index.ts +++ b/packages/components/src/renderers/complex/index.ts @@ -2,3 +2,4 @@ import './carousel'; import './scroll-area'; import './resizable'; import './table'; +import './chatbot'; diff --git a/packages/components/src/ui/chatbot.tsx b/packages/components/src/ui/chatbot.tsx new file mode 100644 index 000000000..59e400052 --- /dev/null +++ b/packages/components/src/ui/chatbot.tsx @@ -0,0 +1,240 @@ +import * as React from "react" +import { cn } from "@/lib/utils" +import { Button } from "./button" +import { Input } from "./input" +import { ScrollArea } from "./scroll-area" +import { Avatar, AvatarFallback, AvatarImage } from "./avatar" +import { Send } from "lucide-react" + +// Message type definition +export interface ChatMessage { + id: string + role: "user" | "assistant" | "system" + content: string + timestamp?: string + avatar?: string + avatarFallback?: string +} + +// Chatbot container props +export interface ChatbotProps extends React.HTMLAttributes { + messages?: ChatMessage[] + placeholder?: string + onSendMessage?: (message: string) => void + disabled?: boolean + showTimestamp?: boolean + userAvatar?: string + userAvatarFallback?: string + assistantAvatar?: string + assistantAvatarFallback?: string + maxHeight?: string +} + +// Chatbot container component +const Chatbot = React.forwardRef( + ( + { + className, + messages = [], + placeholder = "Type your message...", + onSendMessage, + disabled = false, + showTimestamp = false, + userAvatar, + userAvatarFallback = "You", + assistantAvatar, + assistantAvatarFallback = "AI", + maxHeight = "500px", + ...props + }, + ref + ) => { + const [inputValue, setInputValue] = React.useState("") + const scrollRef = React.useRef(null) + const inputRef = React.useRef(null) + + // Auto-scroll to bottom when new messages arrive + React.useEffect(() => { + if (scrollRef.current) { + const scrollElement = scrollRef.current.querySelector('[data-radix-scroll-area-viewport]') + if (scrollElement) { + scrollElement.scrollTop = scrollElement.scrollHeight + } + } + }, [messages]) + + const handleSend = () => { + if (inputValue.trim() && onSendMessage) { + onSendMessage(inputValue.trim()) + setInputValue("") + inputRef.current?.focus() + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + return ( +
+ {/* Messages area */} + +
+ {messages.length === 0 ? ( +
+ No messages yet. Start a conversation! +
+ ) : ( + messages.map((message) => ( + + )) + )} +
+
+ + {/* Input area */} +
+
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + disabled={disabled} + className="flex-1" + /> + +
+
+
+ ) + } +) +Chatbot.displayName = "Chatbot" + +// Individual message component +interface ChatMessageProps { + message: ChatMessage + showTimestamp?: boolean + userAvatar?: string + userAvatarFallback?: string + assistantAvatar?: string + assistantAvatarFallback?: string +} + +const ChatMessage: React.FC = ({ + message, + showTimestamp, + userAvatar, + userAvatarFallback, + assistantAvatar, + assistantAvatarFallback, +}) => { + const isUser = message.role === "user" + const isSystem = message.role === "system" + + if (isSystem) { + return ( +
+
+ {message.content} +
+
+ ) + } + + const avatar = isUser + ? (message.avatar || userAvatar) + : (message.avatar || assistantAvatar) + + const avatarFallback = isUser + ? (message.avatarFallback || userAvatarFallback) + : (message.avatarFallback || assistantAvatarFallback) + + return ( +
+ + + {avatarFallback} + + +
+
+

{message.content}

+
+ {showTimestamp && message.timestamp && ( + + {message.timestamp} + + )} +
+
+ ) +} + +// Typing indicator component +export interface TypingIndicatorProps extends React.HTMLAttributes { + avatarSrc?: string + avatarFallback?: string +} + +const TypingIndicator = React.forwardRef( + ({ className, avatarSrc, avatarFallback = "AI", ...props }, ref) => { + return ( +
+ + + {avatarFallback} + +
+
+ + + +
+
+
+ ) + } +) +TypingIndicator.displayName = "TypingIndicator" + +export { Chatbot, TypingIndicator, type ChatMessage as ChatbotMessage } diff --git a/packages/components/src/ui/index.ts b/packages/components/src/ui/index.ts index 7d71e7829..11029e5d4 100644 --- a/packages/components/src/ui/index.ts +++ b/packages/components/src/ui/index.ts @@ -11,6 +11,7 @@ export * from './calendar'; export * from './card'; export * from './carousel'; export * from './chart'; +export * from './chatbot'; export * from './checkbox'; export * from './collapsible'; export * from './command'; From 44936478d011cd17218e090778c4f08cbf8e40d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:47:19 +0000 Subject: [PATCH 3/7] Fix TypeScript types for Chatbot component Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- .../src/renderers/complex/chatbot.test.ts | 18 +++++++++--------- .../src/renderers/complex/chatbot.tsx | 8 ++++---- packages/components/src/ui/chatbot.tsx | 5 +++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/components/src/renderers/complex/chatbot.test.ts b/packages/components/src/renderers/complex/chatbot.test.ts index 607b54568..1d4c029a9 100644 --- a/packages/components/src/renderers/complex/chatbot.test.ts +++ b/packages/components/src/renderers/complex/chatbot.test.ts @@ -8,16 +8,16 @@ describe('Chatbot Component', () => { }); it('should have proper metadata', () => { - const metadata = ComponentRegistry.getMetadata('chatbot'); - expect(metadata).toBeDefined(); - expect(metadata?.label).toBe('Chatbot'); - expect(metadata?.inputs).toBeDefined(); - expect(metadata?.defaultProps).toBeDefined(); + const config = ComponentRegistry.getConfig('chatbot'); + expect(config).toBeDefined(); + expect(config?.label).toBe('Chatbot'); + expect(config?.inputs).toBeDefined(); + expect(config?.defaultProps).toBeDefined(); }); it('should have expected inputs', () => { - const metadata = ComponentRegistry.getMetadata('chatbot'); - const inputNames = metadata?.inputs?.map((input: any) => input.name) || []; + const config = ComponentRegistry.getConfig('chatbot'); + const inputNames = config?.inputs?.map((input: any) => input.name) || []; expect(inputNames).toContain('messages'); expect(inputNames).toContain('placeholder'); @@ -27,8 +27,8 @@ describe('Chatbot Component', () => { }); it('should have sensible default props', () => { - const metadata = ComponentRegistry.getMetadata('chatbot'); - const defaults = metadata?.defaultProps; + const config = ComponentRegistry.getConfig('chatbot'); + const defaults = config?.defaultProps; expect(defaults).toBeDefined(); expect(defaults?.placeholder).toBe('Type your message...'); diff --git a/packages/components/src/renderers/complex/chatbot.tsx b/packages/components/src/renderers/complex/chatbot.tsx index 404c035d8..0aa077684 100644 --- a/packages/components/src/renderers/complex/chatbot.tsx +++ b/packages/components/src/renderers/complex/chatbot.tsx @@ -1,11 +1,11 @@ import { ComponentRegistry } from '@object-ui/core'; -import { Chatbot, ChatbotMessage } from '@/ui'; +import { Chatbot, type ChatMessage } from '@/ui'; import { useState } from 'react'; ComponentRegistry.register('chatbot', ({ schema, className, ...props }) => { // Initialize messages from schema or use empty array - const [messages, setMessages] = useState( + const [messages, setMessages] = useState( schema.messages?.map((msg: any, idx: number) => ({ id: msg.id || `msg-${idx}`, role: msg.role || 'user', @@ -19,7 +19,7 @@ ComponentRegistry.register('chatbot', // Handle sending new messages const handleSendMessage = (content: string) => { // Create user message - const userMessage: ChatbotMessage = { + const userMessage: ChatMessage = { id: `msg-${Date.now()}`, role: 'user', content, @@ -36,7 +36,7 @@ ComponentRegistry.register('chatbot', // Auto-response feature for demo purposes if (schema.autoResponse) { setTimeout(() => { - const assistantMessage: ChatbotMessage = { + const assistantMessage: ChatMessage = { id: `msg-${Date.now()}-response`, role: 'assistant', content: schema.autoResponseText || 'Thank you for your message!', diff --git a/packages/components/src/ui/chatbot.tsx b/packages/components/src/ui/chatbot.tsx index 59e400052..2543a30a5 100644 --- a/packages/components/src/ui/chatbot.tsx +++ b/packages/components/src/ui/chatbot.tsx @@ -139,7 +139,7 @@ const Chatbot = React.forwardRef( Chatbot.displayName = "Chatbot" // Individual message component -interface ChatMessageProps { +export interface ChatMessageProps { message: ChatMessage showTimestamp?: boolean userAvatar?: string @@ -237,4 +237,5 @@ const TypingIndicator = React.forwardRef( ) TypingIndicator.displayName = "TypingIndicator" -export { Chatbot, TypingIndicator, type ChatMessage as ChatbotMessage } +export { Chatbot, TypingIndicator } +export type { ChatMessage, ChatMessage as ChatbotMessage } From ac6fcd7e136ca134dd8e08ba9afde8cfccfd2b9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:52:29 +0000 Subject: [PATCH 4/7] Add ChatbotDemo and update prototype app to showcase the component Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- examples/prototype/src/App.tsx | 393 +----------------- examples/prototype/src/ChatbotDemo.tsx | 196 +++++++++ .../src/renderers/complex/chatbot.test.ts | 7 +- 3 files changed, 205 insertions(+), 391 deletions(-) create mode 100644 examples/prototype/src/ChatbotDemo.tsx diff --git a/examples/prototype/src/App.tsx b/examples/prototype/src/App.tsx index 05cc6851a..c01417107 100644 --- a/examples/prototype/src/App.tsx +++ b/examples/prototype/src/App.tsx @@ -1,396 +1,9 @@ import { SchemaRenderer } from '@object-ui/react'; import '@object-ui/components'; - -const schema = { - type: 'sidebar-provider', - body: [ - { - type: 'sidebar', - body: [ - { - type: 'sidebar-header', - body: [ - { - type: 'div', - className: 'px-4 py-4 font-bold text-xl flex items-center gap-2', - body: [ - { - type: 'image', - src: '/logo.svg', - className: 'size-8 rounded-lg', - alt: 'Object UI Logo' - }, - { - type: 'text', - content: 'Object UI' - } - ] - } - ] - }, - { - type: 'sidebar-content', - body: [ - { - type: 'sidebar-group', - label: 'Platform', - body: [ - { - type: 'sidebar-menu', - body: [ - { - type: 'sidebar-menu-item', - body: { - type: 'sidebar-menu-button', - active: true, - body: [ - { type: 'icon', name: 'SquareTerminal' }, - { type: 'span', body: { type: 'text', content: 'Dashboard' } } - ] - } - }, - { - type: 'sidebar-menu-item', - body: { - type: 'sidebar-menu-button', - body: [ - { type: 'icon', name: 'Frame' }, - { type: 'span', body: { type: 'text', content: 'Projects' } } - ] - } - }, - { - type: 'sidebar-menu-item', - body: { - type: 'sidebar-menu-button', - body: [ - { type: 'icon', name: 'Map' }, - { type: 'span', body: { type: 'text', content: 'Tasks' } } - ] - } - } - ] - } - ] - }, - { - type: 'sidebar-group', - label: 'Settings', - body: [ - { - type: 'sidebar-menu', - body: [ - { - type: 'sidebar-menu-item', - body: { - type: 'sidebar-menu-button', - body: [ - { type: 'icon', name: 'User' }, - { type: 'span', body: { type: 'text', content: 'Profile' } } - ] - } - }, - { - type: 'sidebar-menu-item', - body: { - type: 'sidebar-menu-button', - body: [ - { type: 'icon', name: 'CreditCard' }, - { type: 'span', body: { type: 'text', content: 'Billing' } } - ] - } - } - ] - } - ] - } - ] - }, - { - type: 'sidebar-footer', - body: { - type: 'sidebar-menu', - body: { - type: 'sidebar-menu-item', - body: { - type: 'sidebar-menu-button', - size: 'lg', - className: 'data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground', - body: { - type: 'div', - className: 'flex items-center gap-2 text-left leading-tight', - body: [ - { - type: 'avatar', - src: 'https://github.com/shadcn.png', - alt: '@shadcn', - fallback: 'SC', - className: 'h-8 w-8 rounded-lg' - }, - { - type: 'div', - className: 'grid flex-1 text-left text-sm leading-tight', - body: [ - { type: 'span', className: 'truncate font-semibold', body: {type: 'text', content: 'Troy Su'} }, - { type: 'span', className: 'truncate text-xs', body: {type: 'text', content: 'troy@object-ui.com'} } - ] - } - ] - } - } - } - } - } - ] - }, - { - type: 'sidebar-inset', - body: [ - { - type: 'header-bar', - className: 'border-b px-6 py-3', - crumbs: [ - { label: 'Platform', href: '#' }, - { label: 'Dashboard' } - ] - }, - { - type: 'div', - className: 'flex flex-1 flex-col gap-6 p-8 bg-muted/10 min-h-[calc(100vh-4rem)]', - body: [ - { - type: 'div', - className: 'flex items-center justify-between', - body: [ - { - type: 'div', - className: 'space-y-1', - body: [ - { type: 'div', className: 'text-2xl font-bold tracking-tight', body: { type: 'text', content: 'Dashboard' } }, - { type: 'div', className: 'text-sm text-muted-foreground', body: { type: 'text', content: 'Overview of your project performance and metrics.' } } - ] - }, - { - type: 'div', - className: 'flex items-center gap-2', - body: [ - { type: 'button', label: 'Download', variant: 'outline', size: 'sm' }, - { type: 'button', label: 'Create New', size: 'sm' } - ] - } - ] - }, - { - type: 'div', - className: 'grid gap-6 md:grid-cols-2 lg:grid-cols-4', - body: [ - { - type: 'card', - className: 'shadow-sm hover:shadow-md transition-shadow', - body: [ - { - type: 'div', - className: 'p-6 pb-2', - body: { type: 'div', className: 'text-sm font-medium text-muted-foreground', body: { type: 'text', content: 'Total Revenue' } } - }, - { - type: 'div', - className: 'p-6 pt-0', - body: [ - { type: 'div', className: 'text-2xl font-bold', body: { type: 'text', content: '$45,231.89' } }, - { type: 'div', className: 'text-xs text-muted-foreground mt-1', body: { type: 'text', content: '+20.1% from last month' } } - ] - } - ] - }, - { - type: 'card', - className: 'shadow-sm hover:shadow-md transition-shadow', - body: [ - { - type: 'div', - className: 'p-6 pb-2', - body: { type: 'div', className: 'text-sm font-medium text-muted-foreground', body: { type: 'text', content: 'Subscriptions' } } - }, - { - type: 'div', - className: 'p-6 pt-0', - body: [ - { type: 'div', className: 'text-2xl font-bold', body: { type: 'text', content: '+2,350' } }, - { type: 'div', className: 'text-xs text-muted-foreground mt-1', body: { type: 'text', content: '+180.1% from last month' } } - ] - } - ] - }, - { - type: 'card', - className: 'shadow-sm hover:shadow-md transition-shadow', - body: [ - { - type: 'div', - className: 'p-6 pb-2', - body: { type: 'div', className: 'text-sm font-medium text-muted-foreground', body: { type: 'text', content: 'Sales' } } - }, - { - type: 'div', - className: 'p-6 pt-0', - body: [ - { type: 'div', className: 'text-2xl font-bold', body: { type: 'text', content: '+12,234' } }, - { type: 'div', className: 'text-xs text-muted-foreground mt-1', body: { type: 'text', content: '+19% from last month' } } - ] - } - ] - }, - { - type: 'card', - className: 'shadow-sm hover:shadow-md transition-shadow', - body: [ - { - type: 'div', - className: 'p-6 pb-2', - body: { type: 'div', className: 'text-sm font-medium text-muted-foreground', body: { type: 'text', content: 'Active Now' } } - }, - { - type: 'div', - className: 'p-6 pt-0', - body: [ - { type: 'div', className: 'text-2xl font-bold', body: { type: 'text', content: '+573' } }, - { type: 'div', className: 'text-xs text-muted-foreground mt-1', body: { type: 'text', content: '+201 since last hour' } } - ] - } - ] - } - ] - }, - { - type: 'tabs', - defaultValue: 'overview', - className: 'space-y-6', - items: [ - { - value: 'overview', - label: 'Overview', - body: [ - { - type: 'div', - className: 'grid gap-6 md:grid-cols-2 lg:grid-cols-7', - body: [ - { - type: 'card', - className: 'col-span-4 shadow-sm', - title: 'Interactive Chart', - body: { - type: 'div', - className: 'p-6', - body: { - type: 'chart', - chartType: 'bar', - className: "aspect-auto h-[350px] w-full", - data: [ - { month: "January", desktop: 186, mobile: 80 }, - { month: "February", desktop: 305, mobile: 200 }, - { month: "March", desktop: 237, mobile: 120 }, - { month: "April", desktop: 73, mobile: 190 }, - { month: "May", desktop: 209, mobile: 130 }, - { month: "June", desktop: 214, mobile: 140 }, - ], - config: { - desktop: { - label: "Desktop", - color: "hsl(var(--primary))", - }, - mobile: { - label: "Mobile", - color: "hsl(var(--primary)/0.5)", - }, - }, - xAxisKey: "month", - series: [ - { dataKey: "desktop" }, - { dataKey: "mobile" } - ] - } - } - }, - { - type: 'card', - className: 'col-span-3 shadow-sm', - title: 'Quick Access', - description: 'Common actions and forms.', - body: [ - { - type: 'div', - className: 'p-6 pt-0 space-y-4', - body: [ - { - type: 'input', - label: 'Email Address', - id: 'email', - inputType: 'email', - placeholder: 'm@example.com' - }, - { - type: 'input', - label: 'Workspace Name', - id: 'workspace', - placeholder: 'Acme Inc.' - }, - { - type: 'button', - label: 'Save Preferences', - className: 'w-full mt-2' - } - ] - } - ] - } - ] - }, - { - type: 'card', - className: 'shadow-sm mt-6', - title: 'Recent Activity', - description: 'Your recent component usage history.', - body: { - type: 'div', - className: 'p-6 pt-0', - body: [ - { type: 'div', className: 'text-sm', body: { type: 'text', content: 'User updated the schema at 10:42 AM' } }, - { type: 'div', className: 'text-sm text-muted-foreground', body: { type: 'text', content: 'User created a new component at 09:15 AM' } } - ] - } - } - ] - }, - { - value: 'analytics', - label: 'Analytics', - body: { type: 'text', content: 'Analytics Content' } - }, - { - value: 'reports', - label: 'Reports', - body: { type: 'text', content: 'Reports Content' } - }, - { - value: 'notifications', - label: 'Notifications', - body: { type: 'text', content: 'Notifications Content' } - } - ] - } - ] - } - ] - } - ] -}; +import ChatbotDemo from './ChatbotDemo'; function App() { - return ( - - ) + return ; } -export default App - +export default App; diff --git a/examples/prototype/src/ChatbotDemo.tsx b/examples/prototype/src/ChatbotDemo.tsx new file mode 100644 index 000000000..e96dfd075 --- /dev/null +++ b/examples/prototype/src/ChatbotDemo.tsx @@ -0,0 +1,196 @@ +import { SchemaRenderer } from '@object-ui/react'; +import '@object-ui/components'; + +const chatbotSchema = { + type: 'div', + className: 'min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-8', + body: [ + { + type: 'div', + className: 'max-w-4xl mx-auto space-y-8', + body: [ + // Header + { + type: 'div', + className: 'text-center space-y-4', + body: [ + { + type: 'div', + className: 'text-4xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent', + body: { + type: 'text', + content: 'Object UI Chatbot Component' + } + }, + { + type: 'div', + className: 'text-lg text-muted-foreground', + body: { + type: 'text', + content: 'A fully functional, schema-driven chatbot component' + } + } + ] + }, + + // Chatbot Demo Card + { + type: 'card', + className: 'shadow-2xl', + body: [ + { + type: 'div', + className: 'p-6 border-b', + body: { + type: 'div', + className: 'text-xl font-semibold', + body: { + type: 'text', + content: 'Interactive Demo' + } + } + }, + { + type: 'div', + className: 'p-6', + body: { + type: 'chatbot', + messages: [ + { + id: 'welcome', + role: 'assistant', + content: 'Hello! 👋 Welcome to Object UI Chatbot. I\'m here to help you explore this component. Try sending me a message!', + }, + { + id: 'info', + role: 'assistant', + content: 'This chatbot is built entirely from JSON schema. No React components needed!', + } + ], + placeholder: 'Type your message here...', + showTimestamp: true, + userAvatarFallback: 'You', + assistantAvatarFallback: 'AI', + maxHeight: '600px', + autoResponse: true, + autoResponseText: 'Thanks for your message! This is an automated response demonstrating the chatbot functionality. In a real application, you would connect this to your backend API or AI service.', + autoResponseDelay: 1500, + className: 'w-full' + } + } + ] + }, + + // Features Section + { + type: 'div', + className: 'grid md:grid-cols-2 gap-6', + body: [ + { + type: 'card', + className: 'p-6 shadow-lg', + body: [ + { + type: 'div', + className: 'text-lg font-semibold mb-3', + body: { + type: 'text', + content: '✨ Key Features' + } + }, + { + type: 'list', + items: [ + 'Message bubbles with user/assistant roles', + 'Avatar support for participants', + 'Scrollable message history', + 'Input field with send button', + 'Timestamp display (optional)', + 'Auto-response for demos', + 'Fully customizable styling' + ], + className: 'text-sm' + } + ] + }, + { + type: 'card', + className: 'p-6 shadow-lg', + body: [ + { + type: 'div', + className: 'text-lg font-semibold mb-3', + body: { + type: 'text', + content: '🎨 Schema-Driven' + } + }, + { + type: 'div', + className: 'text-sm space-y-2', + body: [ + { + type: 'text', + content: 'This entire chatbot is defined using pure JSON schema. Configure messages, styling, behavior, and more without writing any React code.' + }, + { + type: 'div', + className: 'mt-4 p-3 bg-muted rounded-lg font-mono text-xs', + body: { + type: 'text', + content: '{ type: "chatbot", messages: [...] }' + } + } + ] + } + ] + } + ] + }, + + // Usage Example + { + type: 'card', + className: 'p-6 shadow-lg', + body: [ + { + type: 'div', + className: 'text-lg font-semibold mb-3', + body: { + type: 'text', + content: '📝 Usage Example' + } + }, + { + type: 'div', + className: 'bg-slate-900 text-slate-50 p-4 rounded-lg overflow-x-auto', + body: { + type: 'text', + content: `{ + "type": "chatbot", + "messages": [ + { + "id": "msg-1", + "role": "assistant", + "content": "Hello! How can I help?" + } + ], + "placeholder": "Type your message...", + "showTimestamp": true, + "autoResponse": true, + "className": "w-full" +}` + } + } + ] + } + ] + } + ] +}; + +function ChatbotDemo() { + return ; +} + +export default ChatbotDemo; diff --git a/packages/components/src/renderers/complex/chatbot.test.ts b/packages/components/src/renderers/complex/chatbot.test.ts index 1d4c029a9..d78d4368b 100644 --- a/packages/components/src/renderers/complex/chatbot.test.ts +++ b/packages/components/src/renderers/complex/chatbot.test.ts @@ -1,7 +1,12 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeAll } from 'vitest'; import { ComponentRegistry } from '@object-ui/core'; describe('Chatbot Component', () => { + // Import all renderers to register them + beforeAll(async () => { + await import('./index'); + }); + it('should be registered in ComponentRegistry', () => { const chatbotRenderer = ComponentRegistry.get('chatbot'); expect(chatbotRenderer).toBeDefined(); From a33180d35456ba787e2bd457409cc18536c0cd2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:53:58 +0000 Subject: [PATCH 5/7] Address code review feedback - fix duplicate type export and onSend callback Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- packages/components/src/renderers/complex/chatbot.tsx | 7 ++++--- packages/components/src/ui/chatbot.tsx | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/components/src/renderers/complex/chatbot.tsx b/packages/components/src/renderers/complex/chatbot.tsx index 0aa077684..4a4688ea0 100644 --- a/packages/components/src/renderers/complex/chatbot.tsx +++ b/packages/components/src/renderers/complex/chatbot.tsx @@ -26,11 +26,12 @@ ComponentRegistry.register('chatbot', timestamp: schema.showTimestamp ? new Date().toLocaleTimeString() : undefined, }; - setMessages((prev) => [...prev, userMessage]); + const updatedMessages = [...messages, userMessage]; + setMessages(updatedMessages); - // If onSend callback is provided in schema, call it + // If onSend callback is provided in schema, call it with updated messages if (schema.onSend) { - schema.onSend(content, messages); + schema.onSend(content, updatedMessages); } // Auto-response feature for demo purposes diff --git a/packages/components/src/ui/chatbot.tsx b/packages/components/src/ui/chatbot.tsx index 2543a30a5..aa3490d94 100644 --- a/packages/components/src/ui/chatbot.tsx +++ b/packages/components/src/ui/chatbot.tsx @@ -238,4 +238,4 @@ const TypingIndicator = React.forwardRef( TypingIndicator.displayName = "TypingIndicator" export { Chatbot, TypingIndicator } -export type { ChatMessage, ChatMessage as ChatbotMessage } +export type { ChatMessage } From fd792f0e2c124a92b30e65ae2cef221804fa5890 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:10:23 +0000 Subject: [PATCH 6/7] Address PR review feedback: rename avatar props, improve type safety, document onSend callback, use crypto.randomUUID() Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- .../src/renderers/complex/chatbot.test.ts | 4 +- .../src/renderers/complex/chatbot.tsx | 40 +++++++++++++------ packages/components/src/ui/chatbot.tsx | 24 +++++------ 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/components/src/renderers/complex/chatbot.test.ts b/packages/components/src/renderers/complex/chatbot.test.ts index d78d4368b..2fe407f8d 100644 --- a/packages/components/src/renderers/complex/chatbot.test.ts +++ b/packages/components/src/renderers/complex/chatbot.test.ts @@ -27,8 +27,8 @@ describe('Chatbot Component', () => { expect(inputNames).toContain('messages'); expect(inputNames).toContain('placeholder'); expect(inputNames).toContain('showTimestamp'); - expect(inputNames).toContain('userAvatar'); - expect(inputNames).toContain('assistantAvatar'); + expect(inputNames).toContain('userAvatarUrl'); + expect(inputNames).toContain('assistantAvatarUrl'); }); it('should have sensible default props', () => { diff --git a/packages/components/src/renderers/complex/chatbot.tsx b/packages/components/src/renderers/complex/chatbot.tsx index 4a4688ea0..cab9e847e 100644 --- a/packages/components/src/renderers/complex/chatbot.tsx +++ b/packages/components/src/renderers/complex/chatbot.tsx @@ -2,11 +2,23 @@ import { ComponentRegistry } from '@object-ui/core'; import { Chatbot, type ChatMessage } from '@/ui'; import { useState } from 'react'; +/** + * Chatbot component for Object UI + * + * @remarks + * This component supports an optional `onSend` callback in the schema: + * - Signature: `onSend(content: string, messages: ChatMessage[]): void` + * - Parameters: + * - content: The message text that was sent + * - messages: Array of all messages including the newly added message + * - Called when: User sends a message via the input field + * - Use case: Connect to backend API or trigger custom actions on message send + */ ComponentRegistry.register('chatbot', ({ schema, className, ...props }) => { // Initialize messages from schema or use empty array const [messages, setMessages] = useState( - schema.messages?.map((msg: any, idx: number) => ({ + schema.messages?.map((msg: Partial, idx: number) => ({ id: msg.id || `msg-${idx}`, role: msg.role || 'user', content: msg.content || '', @@ -18,9 +30,9 @@ ComponentRegistry.register('chatbot', // Handle sending new messages const handleSendMessage = (content: string) => { - // Create user message + // Create user message with robust ID generation const userMessage: ChatMessage = { - id: `msg-${Date.now()}`, + id: crypto.randomUUID(), role: 'user', content, timestamp: schema.showTimestamp ? new Date().toLocaleTimeString() : undefined, @@ -38,7 +50,7 @@ ComponentRegistry.register('chatbot', if (schema.autoResponse) { setTimeout(() => { const assistantMessage: ChatMessage = { - id: `msg-${Date.now()}-response`, + id: crypto.randomUUID(), role: 'assistant', content: schema.autoResponseText || 'Thank you for your message!', timestamp: schema.showTimestamp ? new Date().toLocaleTimeString() : undefined, @@ -55,9 +67,9 @@ ComponentRegistry.register('chatbot', onSendMessage={handleSendMessage} disabled={schema.disabled} showTimestamp={schema.showTimestamp} - userAvatar={schema.userAvatar} + userAvatarUrl={schema.userAvatarUrl} userAvatarFallback={schema.userAvatarFallback} - assistantAvatar={schema.assistantAvatar} + assistantAvatarUrl={schema.assistantAvatarUrl} assistantAvatarFallback={schema.assistantAvatarFallback} maxHeight={schema.maxHeight} className={className} @@ -93,26 +105,30 @@ ComponentRegistry.register('chatbot', defaultValue: false }, { - name: 'userAvatar', + name: 'userAvatarUrl', type: 'string', - label: 'User Avatar URL' + label: 'User Avatar URL', + description: 'URL of the user avatar image' }, { name: 'userAvatarFallback', type: 'string', label: 'User Avatar Fallback', - defaultValue: 'You' + defaultValue: 'You', + description: 'Fallback text shown when user avatar image is not available' }, { - name: 'assistantAvatar', + name: 'assistantAvatarUrl', type: 'string', - label: 'Assistant Avatar URL' + label: 'Assistant Avatar URL', + description: 'URL of the assistant avatar image' }, { name: 'assistantAvatarFallback', type: 'string', label: 'Assistant Avatar Fallback', - defaultValue: 'AI' + defaultValue: 'AI', + description: 'Fallback text shown when assistant avatar image is not available' }, { name: 'maxHeight', diff --git a/packages/components/src/ui/chatbot.tsx b/packages/components/src/ui/chatbot.tsx index aa3490d94..3bd52a7b1 100644 --- a/packages/components/src/ui/chatbot.tsx +++ b/packages/components/src/ui/chatbot.tsx @@ -23,9 +23,9 @@ export interface ChatbotProps extends React.HTMLAttributes { onSendMessage?: (message: string) => void disabled?: boolean showTimestamp?: boolean - userAvatar?: string + userAvatarUrl?: string userAvatarFallback?: string - assistantAvatar?: string + assistantAvatarUrl?: string assistantAvatarFallback?: string maxHeight?: string } @@ -40,9 +40,9 @@ const Chatbot = React.forwardRef( onSendMessage, disabled = false, showTimestamp = false, - userAvatar, + userAvatarUrl, userAvatarFallback = "You", - assistantAvatar, + assistantAvatarUrl, assistantAvatarFallback = "AI", maxHeight = "500px", ...props @@ -101,9 +101,9 @@ const Chatbot = React.forwardRef( key={message.id} message={message} showTimestamp={showTimestamp} - userAvatar={userAvatar} + userAvatarUrl={userAvatarUrl} userAvatarFallback={userAvatarFallback} - assistantAvatar={assistantAvatar} + assistantAvatarUrl={assistantAvatarUrl} assistantAvatarFallback={assistantAvatarFallback} /> )) @@ -142,18 +142,18 @@ Chatbot.displayName = "Chatbot" export interface ChatMessageProps { message: ChatMessage showTimestamp?: boolean - userAvatar?: string + userAvatarUrl?: string userAvatarFallback?: string - assistantAvatar?: string + assistantAvatarUrl?: string assistantAvatarFallback?: string } const ChatMessage: React.FC = ({ message, showTimestamp, - userAvatar, + userAvatarUrl, userAvatarFallback, - assistantAvatar, + assistantAvatarUrl, assistantAvatarFallback, }) => { const isUser = message.role === "user" @@ -170,8 +170,8 @@ const ChatMessage: React.FC = ({ } const avatar = isUser - ? (message.avatar || userAvatar) - : (message.avatar || assistantAvatar) + ? (message.avatar || userAvatarUrl) + : (message.avatar || assistantAvatarUrl) const avatarFallback = isUser ? (message.avatarFallback || userAvatarFallback) From e6f2fc187f5f12be3e2475fb5ad2f87abead04c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 02:33:07 +0000 Subject: [PATCH 7/7] Merge main branch - resolve conflicts and integrate new components Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/playground/src/data/examples.ts | 90 ++- docs/components/calendar-view.md | 121 +++ docs/spec/component-library.md | 1 + examples/prototype/src/FilterBuilderDemo.tsx | 317 ++++++++ examples/prototype/src/main.tsx | 9 +- packages/components/docs/FilterBuilder.md | 268 +++++++ .../metadata/FilterBuilder.component.yml | 39 + packages/components/package.json | 3 + .../components/src/new-components.test.ts | 6 + .../src/renderers/complex/calendar-view.tsx | 218 ++++++ .../src/renderers/complex/filter-builder.tsx | 67 ++ .../components/src/renderers/complex/index.ts | 2 + .../src/renderers/data-display/index.ts | 1 + .../src/renderers/data-display/markdown.tsx | 49 ++ packages/components/src/ui/calendar-view.tsx | 503 +++++++++++++ packages/components/src/ui/filter-builder.tsx | 359 +++++++++ packages/components/src/ui/index.ts | 3 + packages/components/src/ui/markdown.tsx | 64 ++ pnpm-lock.yaml | 701 ++++++++++++++++++ 19 files changed, 2819 insertions(+), 2 deletions(-) create mode 100644 docs/components/calendar-view.md create mode 100644 examples/prototype/src/FilterBuilderDemo.tsx create mode 100644 packages/components/docs/FilterBuilder.md create mode 100644 packages/components/metadata/FilterBuilder.component.yml create mode 100644 packages/components/src/renderers/complex/calendar-view.tsx create mode 100644 packages/components/src/renderers/complex/filter-builder.tsx create mode 100644 packages/components/src/renderers/data-display/markdown.tsx create mode 100644 packages/components/src/ui/calendar-view.tsx create mode 100644 packages/components/src/ui/filter-builder.tsx create mode 100644 packages/components/src/ui/markdown.tsx diff --git a/apps/playground/src/data/examples.ts b/apps/playground/src/data/examples.ts index 1af817fab..9f2e04ff2 100644 --- a/apps/playground/src/data/examples.ts +++ b/apps/playground/src/data/examples.ts @@ -638,6 +638,93 @@ export const examples = { ] } ] +}`, + + // Calendar View - Airtable-style calendar + 'calendar-view': `{ + "type": "div", + "className": "space-y-4", + "body": [ + { + "type": "div", + "className": "space-y-2", + "body": [ + { + "type": "text", + "content": "Calendar View", + "className": "text-2xl font-bold" + }, + { + "type": "text", + "content": "Airtable-style calendar for displaying records as events", + "className": "text-muted-foreground" + } + ] + }, + { + "type": "calendar-view", + "className": "h-[600px] border rounded-lg", + "view": "month", + "titleField": "title", + "startDateField": "start", + "endDateField": "end", + "colorField": "type", + "colorMapping": { + "meeting": "#3b82f6", + "deadline": "#ef4444", + "event": "#10b981", + "holiday": "#8b5cf6" + }, + "data": [ + { + "id": 1, + "title": "Team Standup", + "start": "${new Date(new Date().setHours(9, 0, 0, 0)).toISOString()}", + "end": "${new Date(new Date().setHours(9, 30, 0, 0)).toISOString()}", + "type": "meeting", + "allDay": false + }, + { + "id": 2, + "title": "Project Launch", + "start": "${new Date(new Date().setDate(new Date().getDate() + 3)).toISOString()}", + "type": "deadline", + "allDay": true + }, + { + "id": 3, + "title": "Client Meeting", + "start": "${new Date(new Date().setDate(new Date().getDate() + 5)).toISOString()}", + "end": "${new Date(new Date(new Date().setDate(new Date().getDate() + 5)).setHours(14, 0, 0, 0)).toISOString()}", + "type": "meeting", + "allDay": false + }, + { + "id": 4, + "title": "Team Building Event", + "start": "${new Date(new Date().setDate(new Date().getDate() + 7)).toISOString()}", + "end": "${new Date(new Date().setDate(new Date().getDate() + 9)).toISOString()}", + "type": "event", + "allDay": true + }, + { + "id": 5, + "title": "Code Review", + "start": "${new Date(new Date().setDate(new Date().getDate() + 1)).toISOString()}", + "end": "${new Date(new Date(new Date().setDate(new Date().getDate() + 1)).setHours(15, 0, 0, 0)).toISOString()}", + "type": "meeting", + "allDay": false + }, + { + "id": 6, + "title": "National Holiday", + "start": "${new Date(new Date().setDate(new Date().getDate() + 10)).toISOString()}", + "type": "holiday", + "allDay": true + } + ] + } + ] }` }; @@ -646,5 +733,6 @@ export type ExampleKey = keyof typeof examples; export const exampleCategories = { 'Primitives': ['simple-page', 'input-states', 'button-variants'], 'Layouts': ['grid-layout', 'dashboard', 'tabs-demo'], - 'Forms': ['form-demo'] + 'Forms': ['form-demo'], + 'Data Display': ['calendar-view'] }; diff --git a/docs/components/calendar-view.md b/docs/components/calendar-view.md new file mode 100644 index 000000000..05b491a7e --- /dev/null +++ b/docs/components/calendar-view.md @@ -0,0 +1,121 @@ +# Calendar View Component + +The `calendar-view` component is an Airtable-style calendar for displaying records as events. It provides three view modes: Month, Week, and Day. + +## Features + +- **Multiple View Modes**: Switch between Month, Week, and Day views +- **Flexible Data Mapping**: Map your data fields to event properties +- **Color Coding**: Support for color-coded events with custom color mappings +- **Interactive**: Click on events and dates (with callbacks) +- **Responsive**: Works seamlessly on different screen sizes + +## Basic Usage + +```json +{ + "type": "calendar-view", + "data": [ + { + "id": 1, + "title": "Team Meeting", + "start": "2026-01-13T10:00:00.000Z", + "end": "2026-01-13T11:00:00.000Z", + "color": "#3b82f6" + } + ] +} +``` + +## Properties + +| Property | Type | Default | Description | +|:---|:---|:---|:---| +| `data` | `array` | `[]` | Array of record objects to display as events | +| `view` | `'month' \| 'week' \| 'day'` | `'month'` | Default view mode | +| `titleField` | `string` | `'title'` | Field name to use for event title | +| `startDateField` | `string` | `'start'` | Field name for event start date | +| `endDateField` | `string` | `'end'` | Field name for event end date (optional) | +| `allDayField` | `string` | `'allDay'` | Field name for all-day flag | +| `colorField` | `string` | `'color'` | Field name for event color | +| `colorMapping` | `object` | `{}` | Map field values to colors | +| `allowCreate` | `boolean` | `false` | Allow creating events by clicking on dates | +| `className` | `string` | - | Additional CSS classes | + +## Data Structure + +Each event object in the `data` array should have the following structure: + +```typescript +{ + id: string | number; // Unique identifier + title: string; // Event title (or use custom titleField) + start: string | Date; // Start date/time (ISO string or Date) + end?: string | Date; // End date/time (optional) + allDay?: boolean; // Whether it's an all-day event + color?: string; // Event color (hex or CSS color) + [key: string]: any; // Any other custom data +} +``` + +## Examples + +### Month View with Color Mapping + +```json +{ + "type": "calendar-view", + "className": "h-[600px] border rounded-lg", + "view": "month", + "colorField": "type", + "colorMapping": { + "meeting": "#3b82f6", + "deadline": "#ef4444", + "event": "#10b981" + }, + "data": [ + { + "id": 1, + "title": "Team Standup", + "start": "2026-01-13T09:00:00.000Z", + "end": "2026-01-13T09:30:00.000Z", + "type": "meeting" + }, + { + "id": 2, + "title": "Project Deadline", + "start": "2026-01-20T00:00:00.000Z", + "type": "deadline", + "allDay": true + } + ] +} +``` + +## View Modes + +### Month View +Displays a full month calendar grid with events shown as colored bars on their respective dates. Perfect for getting a high-level overview of the month. + +### Week View +Shows a week at a time with each day in a column. Events display with their times, ideal for detailed weekly planning. + +### Day View +Displays a single day with hourly time slots from 12 AM to 11 PM. Events are positioned at their scheduled times, great for detailed daily schedules. + +## Events & Callbacks + +The calendar view supports several event callbacks through the `onAction` mechanism: + +- `event_click`: Triggered when an event is clicked +- `date_click`: Triggered when a date cell is clicked +- `view_change`: Triggered when the view mode changes +- `navigate`: Triggered when navigating between dates + +## Styling + +The calendar view is fully styled with Tailwind CSS and supports custom styling through the `className` prop. + +## Integration with ObjectQL + +When used with ObjectQL, the calendar view can automatically fetch and display records from your database. diff --git a/docs/spec/component-library.md b/docs/spec/component-library.md index 3f021e2e4..62e0ab692 100644 --- a/docs/spec/component-library.md +++ b/docs/spec/component-library.md @@ -68,6 +68,7 @@ Components for visualizing data. | `alert` | Highlighted message | `variant`, `title`, `description` | | `table` | Data-driven table | `columns`, `data`, `caption`, `footer` | | `carousel` | Slideshow component | `items`, `orientation`, `showArrows` | +| `calendar-view` | Airtable-style calendar | `data`, `view`, `titleField`, `startDateField`, `endDateField`, `colorField` | ## 6. Feedback Components Indicators for system status or feedback. diff --git a/examples/prototype/src/FilterBuilderDemo.tsx b/examples/prototype/src/FilterBuilderDemo.tsx new file mode 100644 index 000000000..3d57d15be --- /dev/null +++ b/examples/prototype/src/FilterBuilderDemo.tsx @@ -0,0 +1,317 @@ +import { SchemaRenderer } from '@object-ui/react'; +import '@object-ui/components'; + +const filterBuilderSchema = { + type: 'div', + className: 'min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-8', + body: [ + { + type: 'div', + className: 'max-w-5xl mx-auto space-y-8', + body: [ + // Header + { + type: 'div', + className: 'space-y-2', + body: [ + { + type: 'div', + className: 'text-3xl font-bold tracking-tight', + body: { type: 'text', content: 'Filter Builder Demo' } + }, + { + type: 'div', + className: 'text-muted-foreground', + body: { + type: 'text', + content: 'Airtable-like filter component with advanced field types and operators' + } + } + ] + }, + + // Example 1: User Data Filtering with Date and Select + { + type: 'card', + className: 'shadow-lg', + body: [ + { + type: 'div', + className: 'p-6 border-b', + body: [ + { + type: 'div', + className: 'text-xl font-semibold', + body: { type: 'text', content: 'User Data Filters' } + }, + { + type: 'div', + className: 'text-sm text-muted-foreground mt-1', + body: { + type: 'text', + content: 'Advanced filtering with date, select, and boolean fields' + } + } + ] + }, + { + type: 'div', + className: 'p-6', + body: { + type: 'filter-builder', + name: 'userFilters', + label: 'User Filters', + fields: [ + { value: 'name', label: 'Name', type: 'text' }, + { value: 'email', label: 'Email', type: 'text' }, + { value: 'age', label: 'Age', type: 'number' }, + { + value: 'status', + label: 'Status', + type: 'select', + options: [ + { value: 'active', label: 'Active' }, + { value: 'inactive', label: 'Inactive' }, + { value: 'pending', label: 'Pending' } + ] + }, + { + value: 'department', + label: 'Department', + type: 'select', + options: [ + { value: 'engineering', label: 'Engineering' }, + { value: 'sales', label: 'Sales' }, + { value: 'marketing', label: 'Marketing' }, + { value: 'support', label: 'Support' } + ] + }, + { value: 'joinDate', label: 'Join Date', type: 'date' }, + { value: 'isVerified', label: 'Is Verified', type: 'boolean' } + ], + value: { + id: 'root', + logic: 'and', + conditions: [ + { + id: 'cond-1', + field: 'status', + operator: 'equals', + value: 'active' + } + ] + } + } + } + ] + }, + { + type: 'card', + className: 'shadow-lg', + body: [ + { + type: 'div', + className: 'p-6 border-b', + body: [ + { + type: 'div', + className: 'text-xl font-semibold', + body: { type: 'text', content: 'Product Filters' } + }, + { + type: 'div', + className: 'text-sm text-muted-foreground mt-1', + body: { + type: 'text', + content: 'Filter products by name, price, category, and stock' + } + } + ] + }, + { + type: 'div', + className: 'p-6', + body: { + type: 'filter-builder', + name: 'productFilters', + label: 'Product Filters', + fields: [ + { value: 'name', label: 'Product Name', type: 'text' }, + { value: 'price', label: 'Price', type: 'number' }, + { value: 'category', label: 'Category', type: 'text' }, + { value: 'stock', label: 'Stock Quantity', type: 'number' }, + { value: 'brand', label: 'Brand', type: 'text' }, + { value: 'rating', label: 'Rating', type: 'number' } + ], + value: { + id: 'root', + logic: 'or', + conditions: [ + { + id: 'cond-1', + field: 'price', + operator: 'lessThan', + value: '100' + }, + { + id: 'cond-2', + field: 'category', + operator: 'equals', + value: 'Electronics' + } + ] + } + } + } + ] + }, + + // Example 3: Empty Filter Builder + { + type: 'card', + className: 'shadow-lg', + body: [ + { + type: 'div', + className: 'p-6 border-b', + body: [ + { + type: 'div', + className: 'text-xl font-semibold', + body: { type: 'text', content: 'Order Filters' } + }, + { + type: 'div', + className: 'text-sm text-muted-foreground mt-1', + body: { + type: 'text', + content: 'Start with an empty filter - try adding conditions!' + } + } + ] + }, + { + type: 'div', + className: 'p-6', + body: { + type: 'filter-builder', + name: 'orderFilters', + label: 'Order Filters', + fields: [ + { value: 'orderId', label: 'Order ID', type: 'text' }, + { value: 'customer', label: 'Customer Name', type: 'text' }, + { value: 'total', label: 'Total Amount', type: 'number' }, + { value: 'status', label: 'Order Status', type: 'text' }, + { value: 'date', label: 'Order Date', type: 'text' }, + { value: 'shipped', label: 'Shipped', type: 'boolean' } + ], + value: { + id: 'root', + logic: 'and', + conditions: [] + } + } + } + ] + }, + + // Features section + { + type: 'card', + className: 'shadow-lg bg-primary/5 border-primary/20', + body: { + type: 'div', + className: 'p-6', + body: [ + { + type: 'div', + className: 'text-lg font-semibold mb-4', + body: { type: 'text', content: '✨ Features' } + }, + { + type: 'div', + className: 'grid md:grid-cols-2 gap-4 text-sm', + body: [ + { + type: 'div', + className: 'flex items-start gap-2', + body: [ + { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' }, + { type: 'div', body: { type: 'text', content: 'Dynamic add/remove filter conditions' } } + ] + }, + { + type: 'div', + className: 'flex items-start gap-2', + body: [ + { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' }, + { type: 'div', body: { type: 'text', content: 'Field-type aware operators' } } + ] + }, + { + type: 'div', + className: 'flex items-start gap-2', + body: [ + { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' }, + { type: 'div', body: { type: 'text', content: 'AND/OR logic toggling' } } + ] + }, + { + type: 'div', + className: 'flex items-start gap-2', + body: [ + { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' }, + { type: 'div', body: { type: 'text', content: 'Date & Select field support' } } + ] + }, + { + type: 'div', + className: 'flex items-start gap-2', + body: [ + { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' }, + { type: 'div', body: { type: 'text', content: 'Clear all filters button' } } + ] + }, + { + type: 'div', + className: 'flex items-start gap-2', + body: [ + { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' }, + { type: 'div', body: { type: 'text', content: 'Tailwind CSS styled' } } + ] + }, + { + type: 'div', + className: 'flex items-start gap-2', + body: [ + { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' }, + { type: 'div', body: { type: 'text', content: 'Shadcn UI components' } } + ] + }, + { + type: 'div', + className: 'flex items-start gap-2', + body: [ + { type: 'div', body: { type: 'text', content: '✓' }, className: 'text-primary font-bold' }, + { type: 'div', body: { type: 'text', content: 'Schema-driven configuration' } } + ] + } + ] + } + ] + } + } + ] + } + ] +}; + +function FilterBuilderDemo() { + return ( + + ); +} + +export default FilterBuilderDemo; diff --git a/examples/prototype/src/main.tsx b/examples/prototype/src/main.tsx index bef5202a3..3ce6029b4 100644 --- a/examples/prototype/src/main.tsx +++ b/examples/prototype/src/main.tsx @@ -2,9 +2,16 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +import FilterBuilderDemo from './FilterBuilderDemo.tsx' + +// Check if URL parameter specifies which demo to show +const urlParams = new URLSearchParams(window.location.search); +const demo = urlParams.get('demo'); + +const DemoApp = demo === 'filter-builder' ? FilterBuilderDemo : App; createRoot(document.getElementById('root')!).render( - + , ) diff --git a/packages/components/docs/FilterBuilder.md b/packages/components/docs/FilterBuilder.md new file mode 100644 index 000000000..786aa3a8a --- /dev/null +++ b/packages/components/docs/FilterBuilder.md @@ -0,0 +1,268 @@ +# Filter Builder Component + +An Airtable-like filter builder component for building complex query conditions in Object UI. + +## Overview + +The Filter Builder component provides a user-friendly interface for creating and managing filter conditions. It supports: + +- ✅ Dynamic add/remove filter conditions +- ✅ Field selection from configurable list +- ✅ Type-aware operators (text, number, boolean, date, select) +- ✅ AND/OR logic toggling +- ✅ Clear all filters button +- ✅ Date picker support for date fields +- ✅ Dropdown support for select fields +- ✅ Schema-driven configuration +- ✅ Tailwind CSS styled with Shadcn UI components + +## Usage + +### Basic Example + +```typescript +{ + type: 'filter-builder', + name: 'userFilters', + label: 'User Filters', + fields: [ + { value: 'name', label: 'Name', type: 'text' }, + { value: 'email', label: 'Email', type: 'text' }, + { value: 'age', label: 'Age', type: 'number' }, + { value: 'status', label: 'Status', type: 'text' } + ], + value: { + id: 'root', + logic: 'and', + conditions: [ + { + id: 'cond-1', + field: 'status', + operator: 'equals', + value: 'active' + } + ] + } +} +``` + +### With Select Fields + +```typescript +{ + type: 'filter-builder', + name: 'productFilters', + label: 'Product Filters', + fields: [ + { value: 'name', label: 'Product Name', type: 'text' }, + { value: 'price', label: 'Price', type: 'number' }, + { + value: 'category', + label: 'Category', + type: 'select', + options: [ + { value: 'electronics', label: 'Electronics' }, + { value: 'clothing', label: 'Clothing' }, + { value: 'food', label: 'Food' } + ] + }, + { value: 'inStock', label: 'In Stock', type: 'boolean' } + ], + value: { + id: 'root', + logic: 'and', + conditions: [] + } +} +``` + +### With Date Fields + +```typescript +{ + type: 'filter-builder', + name: 'orderFilters', + label: 'Order Filters', + fields: [ + { value: 'orderId', label: 'Order ID', type: 'text' }, + { value: 'amount', label: 'Amount', type: 'number' }, + { value: 'orderDate', label: 'Order Date', type: 'date' }, + { value: 'shipped', label: 'Shipped', type: 'boolean' } + ], + showClearAll: true +} +``` + +## Props + +### Schema Properties + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `type` | `string` | ✅ | Must be `'filter-builder'` | +| `name` | `string` | ✅ | Form field name for the filter value | +| `label` | `string` | ❌ | Label displayed above the filter builder | +| `fields` | `Field[]` | ✅ | Array of available fields for filtering | +| `value` | `FilterGroup` | ❌ | Initial filter configuration | +| `showClearAll` | `boolean` | ❌ | Show "Clear all" button (default: true) | + +### Field Type + +```typescript +interface Field { + value: string; // Field identifier + label: string; // Display label + type?: string; // Field type: 'text' | 'number' | 'boolean' | 'date' | 'select' + options?: Array<{ value: string; label: string }> // For select fields +} +``` + +### FilterGroup Type + +```typescript +interface FilterGroup { + id: string; // Group identifier + logic: 'and' | 'or'; // Logic operator + conditions: FilterCondition[]; // Array of conditions +} + +interface FilterCondition { + id: string; // Condition identifier + field: string; // Field value + operator: string; // Operator (see below) + value: string | number | boolean; // Filter value +} +``` + +## Operators + +The available operators change based on the field type: + +### Text Fields +- `equals` - Equals +- `notEquals` - Does not equal +- `contains` - Contains +- `notContains` - Does not contain +- `isEmpty` - Is empty +- `isNotEmpty` - Is not empty + +### Number Fields +- `equals` - Equals +- `notEquals` - Does not equal +- `greaterThan` - Greater than +- `lessThan` - Less than +- `greaterOrEqual` - Greater than or equal +- `lessOrEqual` - Less than or equal +- `isEmpty` - Is empty +- `isNotEmpty` - Is not empty + +### Boolean Fields +- `equals` - Equals +- `notEquals` - Does not equal + +### Date Fields +- `equals` - Equals +- `notEquals` - Does not equal +- `before` - Before +- `after` - After +- `between` - Between +- `isEmpty` - Is empty +- `isNotEmpty` - Is not empty + +### Select Fields +- `equals` - Equals +- `notEquals` - Does not equal +- `in` - In +- `notIn` - Not in +- `isEmpty` - Is empty +- `isNotEmpty` - Is not empty + +## Events + +The component emits change events when the filter configuration is modified: + +```typescript +{ + target: { + name: 'filters', + value: { + id: 'root', + logic: 'and', + conditions: [...] + } + } +} +``` + +## Demo + +To see the filter builder in action: + +```bash +pnpm --filter prototype dev +# Visit http://localhost:5173/?demo=filter-builder +``` + +## Styling + +The component is fully styled with Tailwind CSS and follows the Object UI design system. All Shadcn UI components are used for consistent look and feel. + +You can customize the appearance using the `className` prop or by overriding Tailwind classes. + +## Integration + +The Filter Builder integrates seamlessly with Object UI's schema system and can be used in: + +- Forms +- Data tables +- Search interfaces +- Admin panels +- Dashboard filters + +## Example in Context + +```typescript +const pageSchema = { + type: 'page', + title: 'User Management', + body: [ + { + type: 'card', + body: [ + { + type: 'filter-builder', + name: 'userFilters', + label: 'Filter Users', + fields: [ + { value: 'name', label: 'Name', type: 'text' }, + { value: 'email', label: 'Email', type: 'text' }, + { value: 'age', label: 'Age', type: 'number' }, + { value: 'department', label: 'Department', type: 'text' }, + { value: 'active', label: 'Active', type: 'boolean' } + ] + } + ] + }, + { + type: 'table', + // table configuration... + } + ] +}; +``` + +## Technical Details + +- Built with React 18+ hooks +- Uses Radix UI primitives (Select, Popover) +- Type-safe with TypeScript +- Accessible keyboard navigation +- Responsive design + +## Browser Support + +Works in all modern browsers that support: +- ES6+ +- CSS Grid +- Flexbox +- crypto.randomUUID() diff --git a/packages/components/metadata/FilterBuilder.component.yml b/packages/components/metadata/FilterBuilder.component.yml new file mode 100644 index 000000000..5125c0cf7 --- /dev/null +++ b/packages/components/metadata/FilterBuilder.component.yml @@ -0,0 +1,39 @@ +name: FilterBuilder +label: Filter Builder +description: Airtable-like filter builder for creating complex query conditions +category: complex +version: 1.0.0 +framework: react + +props: + - name: label + type: string + description: Label text displayed above the filter builder + - name: name + type: string + required: true + description: Form field name for the filter value + - name: fields + type: array + required: true + description: Available fields for filtering + schema: + - value: string + - label: string + - type: string + - name: value + type: object + description: Current filter configuration + schema: + - id: string + - logic: enum[and, or] + - conditions: array + +events: + - name: onChange + payload: "{ name: string, value: FilterGroup }" + +features: + dynamic_conditions: true + multiple_operators: true + field_type_aware: true diff --git a/packages/components/package.json b/packages/components/package.json index ad9389bbf..2e9aa4509 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -57,8 +57,11 @@ "next-themes": "^0.4.6", "react-day-picker": "^9.13.0", "react-hook-form": "^7.71.0", + "react-markdown": "^10.1.0", "react-resizable-panels": "^4.4.0", "recharts": "^3.6.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", diff --git a/packages/components/src/new-components.test.ts b/packages/components/src/new-components.test.ts index da8061541..a04c518f3 100644 --- a/packages/components/src/new-components.test.ts +++ b/packages/components/src/new-components.test.ts @@ -33,6 +33,12 @@ describe('New Components Registration', () => { expect(component).toBeDefined(); expect(component?.label).toBe('Tree View'); }); + + it('should register markdown component', () => { + const component = ComponentRegistry.getConfig('markdown'); + expect(component).toBeDefined(); + expect(component?.label).toBe('Markdown'); + }); }); describe('Layout Components', () => { diff --git a/packages/components/src/renderers/complex/calendar-view.tsx b/packages/components/src/renderers/complex/calendar-view.tsx new file mode 100644 index 000000000..b620c9956 --- /dev/null +++ b/packages/components/src/renderers/complex/calendar-view.tsx @@ -0,0 +1,218 @@ +import { ComponentRegistry } from '@object-ui/core'; +import { CalendarView, type CalendarEvent } from '@/ui'; +import React from 'react'; + +// Calendar View Renderer - Airtable-style calendar for displaying records as events +ComponentRegistry.register('calendar-view', + ({ schema, className, onAction, ...props }) => { + // Transform schema data to CalendarEvent format + const events: CalendarEvent[] = React.useMemo(() => { + if (!schema.data || !Array.isArray(schema.data)) return []; + + return schema.data.map((record: any, index: number) => { + /** Field name to use for event title display */ + const titleField = schema.titleField || 'title'; + /** Field name containing the event start date/time */ + const startField = schema.startDateField || 'start'; + /** Field name containing the event end date/time (optional) */ + const endField = schema.endDateField || 'end'; + /** Field name to determine event color or color category */ + const colorField = schema.colorField || 'color'; + /** Field name indicating if event is all-day */ + const allDayField = schema.allDayField || 'allDay'; + + const title = record[titleField] || 'Untitled'; + const start = record[startField] ? new Date(record[startField]) : new Date(); + const end = record[endField] ? new Date(record[endField]) : undefined; + const allDay = record[allDayField] !== undefined ? record[allDayField] : false; + + // Handle color mapping + let color = record[colorField]; + if (color && schema.colorMapping && schema.colorMapping[color]) { + color = schema.colorMapping[color]; + } + + return { + id: record.id || record._id || index, + title, + start, + end, + allDay, + color, + data: record, + }; + }); + }, [schema.data, schema.titleField, schema.startDateField, schema.endDateField, schema.colorField, schema.allDayField, schema.colorMapping]); + + const handleEventClick = React.useCallback((event: CalendarEvent) => { + if (onAction) { + onAction({ + type: 'event_click', + payload: { event: event.data, eventId: event.id } + }); + } + if (schema.onEventClick) { + schema.onEventClick(event.data); + } + }, [onAction, schema]); + + const handleDateClick = React.useCallback((date: Date) => { + if (onAction) { + onAction({ + type: 'date_click', + payload: { date } + }); + } + if (schema.onDateClick) { + schema.onDateClick(date); + } + }, [onAction, schema]); + + const handleViewChange = React.useCallback((view: "month" | "week" | "day") => { + if (onAction) { + onAction({ + type: 'view_change', + payload: { view } + }); + } + if (schema.onViewChange) { + schema.onViewChange(view); + } + }, [onAction, schema]); + + const handleNavigate = React.useCallback((date: Date) => { + if (onAction) { + onAction({ + type: 'navigate', + payload: { date } + }); + } + if (schema.onNavigate) { + schema.onNavigate(date); + } + }, [onAction, schema]); + + return ( + + ); + }, + { + label: 'Calendar View', + inputs: [ + { + name: 'data', + type: 'array', + label: 'Data', + description: 'Array of record objects to display as events' + }, + { + name: 'titleField', + type: 'string', + label: 'Title Field', + defaultValue: 'title', + description: 'Field name to use for event title' + }, + { + name: 'startDateField', + type: 'string', + label: 'Start Date Field', + defaultValue: 'start', + description: 'Field name for event start date' + }, + { + name: 'endDateField', + type: 'string', + label: 'End Date Field', + defaultValue: 'end', + description: 'Field name for event end date (optional)' + }, + { + name: 'allDayField', + type: 'string', + label: 'All Day Field', + defaultValue: 'allDay', + description: 'Field name for all-day flag' + }, + { + name: 'colorField', + type: 'string', + label: 'Color Field', + defaultValue: 'color', + description: 'Field name for event color' + }, + { + name: 'colorMapping', + type: 'object', + label: 'Color Mapping', + description: 'Map field values to colors (e.g., {meeting: "blue", deadline: "red"})' + }, + { + name: 'view', + type: 'enum', + enum: ['month', 'week', 'day'], + defaultValue: 'month', + label: 'View Mode', + description: 'Calendar view mode (month, week, or day)' + }, + { + name: 'currentDate', + type: 'string', + label: 'Current Date', + description: 'ISO date string for initial calendar date' + }, + { + name: 'allowCreate', + type: 'boolean', + label: 'Allow Create', + defaultValue: false, + description: 'Allow creating events by clicking on dates' + }, + { name: 'className', type: 'string', label: 'CSS Class' } + ], + defaultProps: { + view: 'month', + titleField: 'title', + startDateField: 'start', + endDateField: 'end', + allDayField: 'allDay', + colorField: 'color', + allowCreate: false, + data: [ + { + id: 1, + title: 'Team Meeting', + start: new Date(new Date().setHours(10, 0, 0, 0)).toISOString(), + end: new Date(new Date().setHours(11, 0, 0, 0)).toISOString(), + color: '#3b82f6', + allDay: false + }, + { + id: 2, + title: 'Project Deadline', + start: new Date(new Date().setDate(new Date().getDate() + 3)).toISOString(), + color: '#ef4444', + allDay: true + }, + { + id: 3, + title: 'Conference', + start: new Date(new Date().setDate(new Date().getDate() + 7)).toISOString(), + end: new Date(new Date().setDate(new Date().getDate() + 9)).toISOString(), + color: '#10b981', + allDay: true + } + ], + className: 'h-[600px] border rounded-lg' + } + } +); diff --git a/packages/components/src/renderers/complex/filter-builder.tsx b/packages/components/src/renderers/complex/filter-builder.tsx new file mode 100644 index 000000000..0518d2f84 --- /dev/null +++ b/packages/components/src/renderers/complex/filter-builder.tsx @@ -0,0 +1,67 @@ +import { ComponentRegistry } from '@object-ui/core'; +import { FilterBuilder, type FilterGroup } from '@/ui/filter-builder'; + +ComponentRegistry.register('filter-builder', + ({ schema, className, onChange, ...props }) => { + const handleChange = (value: FilterGroup) => { + if (onChange) { + onChange({ + target: { + name: schema.name, + value: value, + }, + }); + } + }; + + return ( +
+ {schema.label && ( + + )} + +
+ ); + }, + { + label: 'Filter Builder', + inputs: [ + { name: 'label', type: 'string', label: 'Label' }, + { name: 'name', type: 'string', label: 'Name', required: true }, + { + name: 'fields', + type: 'array', + label: 'Fields', + description: 'Array of { value: string, label: string, type?: string } objects', + required: true + }, + { + name: 'value', + type: 'object', + label: 'Initial Value', + description: 'FilterGroup object with conditions' + } + ], + defaultProps: { + label: 'Filters', + name: 'filters', + fields: [ + { value: 'name', label: 'Name', type: 'text' }, + { value: 'email', label: 'Email', type: 'text' }, + { value: 'age', label: 'Age', type: 'number' }, + { value: 'status', label: 'Status', type: 'text' } + ], + value: { + id: 'root', + logic: 'and', + conditions: [] + } + } + } +); diff --git a/packages/components/src/renderers/complex/index.ts b/packages/components/src/renderers/complex/index.ts index 599499728..3b4643af7 100644 --- a/packages/components/src/renderers/complex/index.ts +++ b/packages/components/src/renderers/complex/index.ts @@ -1,5 +1,7 @@ import './carousel'; +import './filter-builder'; import './scroll-area'; import './resizable'; import './table'; import './chatbot'; +import './calendar-view'; diff --git a/packages/components/src/renderers/data-display/index.ts b/packages/components/src/renderers/data-display/index.ts index 7b1a3bb0c..2f2843084 100644 --- a/packages/components/src/renderers/data-display/index.ts +++ b/packages/components/src/renderers/data-display/index.ts @@ -4,3 +4,4 @@ import './alert'; import './chart'; import './list'; import './tree-view'; +import './markdown'; diff --git a/packages/components/src/renderers/data-display/markdown.tsx b/packages/components/src/renderers/data-display/markdown.tsx new file mode 100644 index 000000000..b700114f4 --- /dev/null +++ b/packages/components/src/renderers/data-display/markdown.tsx @@ -0,0 +1,49 @@ +import { ComponentRegistry } from '@object-ui/core'; +import { Markdown } from '@/ui'; + +/** + * Markdown Renderer Component + * + * A schema-driven renderer that displays markdown content in Object UI applications. + * This component follows the "Schema First" principle, enabling markdown rendering + * through pure JSON/YAML configuration without writing custom code. + * + * @example + * ```json + * { + * "type": "markdown", + * "content": "# Hello World\n\nThis is **markdown** text." + * } + * ``` + * + * Features: + * - GitHub Flavored Markdown support (tables, strikethrough, task lists) + * - XSS protection via rehype-sanitize + * - Dark mode support + * - Tailwind CSS prose styling + */ +ComponentRegistry.register('markdown', + ({ schema, className, ...props }) => ( + + ), + { + label: 'Markdown', + inputs: [ + { + name: 'content', + type: 'string', + label: 'Markdown Content', + required: true, + inputType: 'textarea' + }, + { name: 'className', type: 'string', label: 'CSS Class' } + ], + defaultProps: { + content: '# Hello World\n\nThis is a **markdown** component with *formatting* support.\n\n- Item 1\n- Item 2\n- Item 3', + } + } +); diff --git a/packages/components/src/ui/calendar-view.tsx b/packages/components/src/ui/calendar-view.tsx new file mode 100644 index 000000000..a58167955 --- /dev/null +++ b/packages/components/src/ui/calendar-view.tsx @@ -0,0 +1,503 @@ +"use client" + +import * as React from "react" +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" +import { cn } from "@/lib/utils" +import { Button } from "@/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/ui/select" + +const DEFAULT_EVENT_COLOR = "bg-blue-500 text-white" + +export interface CalendarEvent { + id: string | number + title: string + start: Date + end?: Date + allDay?: boolean + color?: string + data?: any +} + +export interface CalendarViewProps { + events?: CalendarEvent[] + view?: "month" | "week" | "day" + currentDate?: Date + onEventClick?: (event: CalendarEvent) => void + onDateClick?: (date: Date) => void + onViewChange?: (view: "month" | "week" | "day") => void + onNavigate?: (date: Date) => void + className?: string +} + +function CalendarView({ + events = [], + view = "month", + currentDate = new Date(), + onEventClick, + onDateClick, + onViewChange, + onNavigate, + className, +}: CalendarViewProps) { + const [selectedView, setSelectedView] = React.useState(view) + const [selectedDate, setSelectedDate] = React.useState(currentDate) + + const handlePrevious = () => { + const newDate = new Date(selectedDate) + if (selectedView === "month") { + newDate.setMonth(newDate.getMonth() - 1) + } else if (selectedView === "week") { + newDate.setDate(newDate.getDate() - 7) + } else { + newDate.setDate(newDate.getDate() - 1) + } + setSelectedDate(newDate) + onNavigate?.(newDate) + } + + const handleNext = () => { + const newDate = new Date(selectedDate) + if (selectedView === "month") { + newDate.setMonth(newDate.getMonth() + 1) + } else if (selectedView === "week") { + newDate.setDate(newDate.getDate() + 7) + } else { + newDate.setDate(newDate.getDate() + 1) + } + setSelectedDate(newDate) + onNavigate?.(newDate) + } + + const handleToday = () => { + const today = new Date() + setSelectedDate(today) + onNavigate?.(today) + } + + const handleViewChange = (newView: "month" | "week" | "day") => { + setSelectedView(newView) + onViewChange?.(newView) + } + + const getDateLabel = () => { + if (selectedView === "month") { + return selectedDate.toLocaleDateString("default", { + month: "long", + year: "numeric", + }) + } else if (selectedView === "week") { + const weekStart = getWeekStart(selectedDate) + const weekEnd = new Date(weekStart) + weekEnd.setDate(weekEnd.getDate() + 6) + return `${weekStart.toLocaleDateString("default", { + month: "short", + day: "numeric", + })} - ${weekEnd.toLocaleDateString("default", { + month: "short", + day: "numeric", + year: "numeric", + })}` + } else { + return selectedDate.toLocaleDateString("default", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }) + } + } + + return ( +
+ {/* Header */} +
+
+ +
+ + +
+

{getDateLabel()}

+
+
+ +
+
+ + {/* Calendar Grid */} +
+ {selectedView === "month" && ( + + )} + {selectedView === "week" && ( + + )} + {selectedView === "day" && ( + + )} +
+
+ ) +} + +function getWeekStart(date: Date): Date { + const d = new Date(date) + const day = d.getDay() + const diff = d.getDate() - day + return new Date(d.setDate(diff)) +} + +function getMonthDays(date: Date): Date[] { + const year = date.getFullYear() + const month = date.getMonth() + const firstDay = new Date(year, month, 1) + const lastDay = new Date(year, month + 1, 0) + const startDay = firstDay.getDay() + const days: Date[] = [] + + // Add previous month days + for (let i = startDay - 1; i >= 0; i--) { + const prevDate = new Date(firstDay) + prevDate.setDate(prevDate.getDate() - (i + 1)) + days.push(prevDate) + } + + // Add current month days + for (let i = 1; i <= lastDay.getDate(); i++) { + days.push(new Date(year, month, i)) + } + + // Add next month days + const remainingDays = 42 - days.length + for (let i = 1; i <= remainingDays; i++) { + const nextDate = new Date(lastDay) + nextDate.setDate(nextDate.getDate() + i) + days.push(nextDate) + } + + return days +} + +function isSameDay(date1: Date, date2: Date): boolean { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ) +} + +function getEventsForDate(date: Date, events: CalendarEvent[]): CalendarEvent[] { + return events.filter((event) => { + const eventStart = new Date(event.start) + const eventEnd = event.end ? new Date(event.end) : new Date(eventStart) + + // Create new date objects for comparison to avoid mutation + const dateStart = new Date(date) + dateStart.setHours(0, 0, 0, 0) + const dateEnd = new Date(date) + dateEnd.setHours(23, 59, 59, 999) + + const eventStartTime = new Date(eventStart) + eventStartTime.setHours(0, 0, 0, 0) + const eventEndTime = new Date(eventEnd) + eventEndTime.setHours(23, 59, 59, 999) + + return dateStart <= eventEndTime && dateEnd >= eventStartTime + }) +} + +interface MonthViewProps { + date: Date + events: CalendarEvent[] + onEventClick?: (event: CalendarEvent) => void + onDateClick?: (date: Date) => void +} + +function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps) { + const days = getMonthDays(date) + const today = new Date() + const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + + return ( +
+ {/* Week day headers */} +
+ {weekDays.map((day) => ( +
+ {day} +
+ ))} +
+ + {/* Calendar days */} +
+ {days.map((day, index) => { + const dayEvents = getEventsForDate(day, events) + const isCurrentMonth = day.getMonth() === date.getMonth() + const isToday = isSameDay(day, today) + + return ( +
onDateClick?.(day)} + > +
+ {day.getDate()} +
+
+ {dayEvents.slice(0, 3).map((event) => ( +
{ + e.stopPropagation() + onEventClick?.(event) + }} + > + {event.title} +
+ ))} + {dayEvents.length > 3 && ( +
+ +{dayEvents.length - 3} more +
+ )} +
+
+ ) + })} +
+
+ ) +} + +interface WeekViewProps { + date: Date + events: CalendarEvent[] + onEventClick?: (event: CalendarEvent) => void + onDateClick?: (date: Date) => void +} + +function WeekView({ date, events, onEventClick, onDateClick }: WeekViewProps) { + const weekStart = getWeekStart(date) + const weekDays = Array.from({ length: 7 }, (_, i) => { + const day = new Date(weekStart) + day.setDate(day.getDate() + i) + return day + }) + const today = new Date() + + return ( +
+ {/* Week day headers */} +
+ {weekDays.map((day) => { + const isToday = isSameDay(day, today) + return ( +
+
+ {day.toLocaleDateString("default", { weekday: "short" })} +
+
+ {day.getDate()} +
+
+ ) + })} +
+ + {/* Week events */} +
+ {weekDays.map((day) => { + const dayEvents = getEventsForDate(day, events) + return ( +
onDateClick?.(day)} + > +
+ {dayEvents.map((event) => ( +
{ + e.stopPropagation() + onEventClick?.(event) + }} + > +
{event.title}
+ {!event.allDay && ( +
+ {event.start.toLocaleTimeString("default", { + hour: "numeric", + minute: "2-digit", + })} +
+ )} +
+ ))} +
+
+ ) + })} +
+
+ ) +} + +interface DayViewProps { + date: Date + events: CalendarEvent[] + onEventClick?: (event: CalendarEvent) => void +} + +function DayView({ date, events, onEventClick }: DayViewProps) { + const dayEvents = getEventsForDate(date, events) + const hours = Array.from({ length: 24 }, (_, i) => i) + + return ( +
+
+ {hours.map((hour) => { + const hourEvents = dayEvents.filter((event) => { + if (event.allDay) return hour === 0 + const eventHour = event.start.getHours() + return eventHour === hour + }) + + return ( +
+
+ {hour === 0 + ? "12 AM" + : hour < 12 + ? `${hour} AM` + : hour === 12 + ? "12 PM" + : `${hour - 12} PM`} +
+
+ {hourEvents.map((event) => ( +
onEventClick?.(event)} + > +
{event.title}
+ {!event.allDay && ( +
+ {event.start.toLocaleTimeString("default", { + hour: "numeric", + minute: "2-digit", + })} + {event.end && + ` - ${event.end.toLocaleTimeString("default", { + hour: "numeric", + minute: "2-digit", + })}`} +
+ )} +
+ ))} +
+
+ ) + })} +
+
+ ) +} + +export { CalendarView } diff --git a/packages/components/src/ui/filter-builder.tsx b/packages/components/src/ui/filter-builder.tsx new file mode 100644 index 000000000..8c973c85f --- /dev/null +++ b/packages/components/src/ui/filter-builder.tsx @@ -0,0 +1,359 @@ +"use client" + +import * as React from "react" +import { X, Plus, Trash2 } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/ui/button" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui/select" +import { Input } from "@/ui/input" + +export interface FilterCondition { + id: string + field: string + operator: string + value: string | number | boolean +} + +export interface FilterGroup { + id: string + logic: "and" | "or" + conditions: FilterCondition[] +} + +export interface FilterBuilderProps { + fields?: Array<{ + value: string + label: string + type?: string + options?: Array<{ value: string; label: string }> // For select fields + }> + value?: FilterGroup + onChange?: (value: FilterGroup) => void + className?: string + showClearAll?: boolean +} + +const defaultOperators = [ + { value: "equals", label: "Equals" }, + { value: "notEquals", label: "Does not equal" }, + { value: "contains", label: "Contains" }, + { value: "notContains", label: "Does not contain" }, + { value: "isEmpty", label: "Is empty" }, + { value: "isNotEmpty", label: "Is not empty" }, + { value: "greaterThan", label: "Greater than" }, + { value: "lessThan", label: "Less than" }, + { value: "greaterOrEqual", label: "Greater than or equal" }, + { value: "lessOrEqual", label: "Less than or equal" }, + { value: "before", label: "Before" }, + { value: "after", label: "After" }, + { value: "between", label: "Between" }, + { value: "in", label: "In" }, + { value: "notIn", label: "Not in" }, +] + +const textOperators = ["equals", "notEquals", "contains", "notContains", "isEmpty", "isNotEmpty"] +const numberOperators = ["equals", "notEquals", "greaterThan", "lessThan", "greaterOrEqual", "lessOrEqual", "isEmpty", "isNotEmpty"] +const booleanOperators = ["equals", "notEquals"] +const dateOperators = ["equals", "notEquals", "before", "after", "between", "isEmpty", "isNotEmpty"] +const selectOperators = ["equals", "notEquals", "in", "notIn", "isEmpty", "isNotEmpty"] + +function FilterBuilder({ + fields = [], + value, + onChange, + className, + showClearAll = true, +}: FilterBuilderProps) { + const [filterGroup, setFilterGroup] = React.useState( + value || { + id: "root", + logic: "and", + conditions: [], + } + ) + + React.useEffect(() => { + if (value && JSON.stringify(value) !== JSON.stringify(filterGroup)) { + setFilterGroup(value) + } + }, [value]) + + const handleChange = (newGroup: FilterGroup) => { + setFilterGroup(newGroup) + onChange?.(newGroup) + } + + const addCondition = () => { + const newCondition: FilterCondition = { + id: crypto.randomUUID(), + field: fields[0]?.value || "", + operator: "equals", + value: "", + } + handleChange({ + ...filterGroup, + conditions: [...filterGroup.conditions, newCondition], + }) + } + + const removeCondition = (conditionId: string) => { + handleChange({ + ...filterGroup, + conditions: filterGroup.conditions.filter((c) => c.id !== conditionId), + }) + } + + const clearAllConditions = () => { + handleChange({ + ...filterGroup, + conditions: [], + }) + } + + const updateCondition = (conditionId: string, updates: Partial) => { + handleChange({ + ...filterGroup, + conditions: filterGroup.conditions.map((c) => + c.id === conditionId ? { ...c, ...updates } : c + ), + }) + } + + const toggleLogic = () => { + handleChange({ + ...filterGroup, + logic: filterGroup.logic === "and" ? "or" : "and", + }) + } + + const getOperatorsForField = (fieldValue: string) => { + const field = fields.find((f) => f.value === fieldValue) + const fieldType = field?.type || "text" + + switch (fieldType) { + case "number": + return defaultOperators.filter((op) => numberOperators.includes(op.value)) + case "boolean": + return defaultOperators.filter((op) => booleanOperators.includes(op.value)) + case "date": + return defaultOperators.filter((op) => dateOperators.includes(op.value)) + case "select": + return defaultOperators.filter((op) => selectOperators.includes(op.value)) + case "text": + default: + return defaultOperators.filter((op) => textOperators.includes(op.value)) + } + } + + const needsValueInput = (operator: string) => { + return !["isEmpty", "isNotEmpty"].includes(operator) + } + + const getInputType = (fieldValue: string) => { + const field = fields.find((f) => f.value === fieldValue) + const fieldType = field?.type || "text" + + switch (fieldType) { + case "number": + return "number" + case "date": + return "date" + default: + return "text" + } + } + + const renderValueInput = (condition: FilterCondition) => { + const field = fields.find((f) => f.value === condition.field) + + // For select fields with options + if (field?.type === "select" && field.options) { + return ( + + ) + } + + // For boolean fields + if (field?.type === "boolean") { + return ( + + ) + } + + // Default input for text, number, date + const inputType = getInputType(condition.field) + + // Format value based on field type + const formatValue = () => { + if (!condition.value) return "" + if (inputType === "date" && typeof condition.value === "string") { + // Ensure date is in YYYY-MM-DD format + return condition.value.split('T')[0] + } + return String(condition.value) + } + + // Handle value change with proper type conversion + const handleValueChange = (newValue: string) => { + let convertedValue: string | number | boolean = newValue + + if (field?.type === "number" && newValue !== "") { + convertedValue = parseFloat(newValue) || 0 + } else if (field?.type === "date") { + convertedValue = newValue // Keep as ISO string + } + + updateCondition(condition.id, { value: convertedValue }) + } + + return ( + handleValueChange(e.target.value)} + /> + ) + } + + return ( +
+
+
+ Where + {filterGroup.conditions.length > 1 && ( + + )} +
+ {showClearAll && filterGroup.conditions.length > 0 && ( + + )} +
+ +
+ {filterGroup.conditions.map((condition) => ( +
+
+
+ +
+ +
+ +
+ + {needsValueInput(condition.operator) && ( +
+ {renderValueInput(condition)} +
+ )} +
+ + +
+ ))} +
+ + +
+ ) +} + +FilterBuilder.displayName = "FilterBuilder" + +export { FilterBuilder } diff --git a/packages/components/src/ui/index.ts b/packages/components/src/ui/index.ts index 11029e5d4..a13187760 100644 --- a/packages/components/src/ui/index.ts +++ b/packages/components/src/ui/index.ts @@ -8,6 +8,7 @@ export * from './breadcrumb'; export * from './button-group'; export * from './button'; export * from './calendar'; +export * from './calendar-view'; export * from './card'; export * from './carousel'; export * from './chart'; @@ -21,6 +22,7 @@ export * from './drawer'; export * from './dropdown-menu'; export * from './empty'; export * from './field'; +export * from './filter-builder'; export * from './form'; export * from './hover-card'; export * from './input-group'; @@ -29,6 +31,7 @@ export * from './input'; export * from './item'; export * from './kbd'; export * from './label'; +export * from './markdown'; export * from './menubar'; export * from './navigation-menu'; export * from './pagination'; diff --git a/packages/components/src/ui/markdown.tsx b/packages/components/src/ui/markdown.tsx new file mode 100644 index 000000000..95b4a789c --- /dev/null +++ b/packages/components/src/ui/markdown.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" +import rehypeSanitize from "rehype-sanitize" +import { cn } from "@/lib/utils" + +/** + * Props for the Markdown component. + * + * This component renders markdown content using react-markdown with GitHub Flavored Markdown support. + * All content is sanitized to prevent XSS attacks. + */ +export interface MarkdownProps { + /** + * The markdown content to render. Supports GitHub Flavored Markdown including: + * - Headers (H1-H6) + * - Bold, italic, and inline code + * - Links and images + * - Lists (ordered, unordered, and nested) + * - Tables + * - Blockquotes + * - Code blocks + */ + content: string + + /** + * Optional CSS class name to apply custom styling to the markdown container. + * The component uses Tailwind CSS prose classes for typography by default. + */ + className?: string +} + +function Markdown({ content, className }: MarkdownProps) { + return ( +
+ + {content} + +
+ ) +} + +export { Markdown } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cbb2e0241..ff6c0586d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -380,12 +380,21 @@ importers: react-hook-form: specifier: ^7.71.0 version: 7.71.0(react@18.3.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.12)(react@18.3.1) react-resizable-panels: specifier: ^4.4.0 version: 4.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) recharts: specifier: ^3.6.0 version: 3.6.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@5.0.1) + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2124,9 +2133,15 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2148,6 +2163,9 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.19.29': resolution: {integrity: sha512-YrT9ArrGaHForBaCNwFjoqJWmn8G1Pr7+BH/vwyLHciA9qT/wSiuOhxGCT50JA5xLvFBd6PIiGkE3afxcPE1nw==} @@ -2163,6 +2181,9 @@ packages: '@types/react@18.3.12': resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -2525,6 +2546,9 @@ packages: peerDependencies: postcss: ^8.1.0 + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2593,6 +2617,12 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} @@ -2741,6 +2771,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + deep-eql@4.1.4: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} @@ -2833,6 +2866,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-plugin-react-hooks@7.0.1: resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==} engines: {node: '>=18'} @@ -2896,6 +2933,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -2920,6 +2960,9 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3053,9 +3096,15 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -3079,6 +3128,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -3131,6 +3183,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -3141,6 +3196,12 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -3149,6 +3210,9 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3157,6 +3221,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -3165,6 +3232,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -3277,6 +3348,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -3322,9 +3396,54 @@ packages: mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} @@ -3335,21 +3454,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + micromark-util-sanitize-uri@2.0.1: resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3461,6 +3649,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} @@ -3626,6 +3817,12 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': 18.3.12 + react: 18.3.1 + react-redux@9.2.0: resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} peerDependencies: @@ -3718,6 +3915,21 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3856,6 +4068,12 @@ packages: strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -3958,6 +4176,9 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -4042,6 +4263,9 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -5971,8 +6195,16 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/hast@3.0.4': @@ -5994,6 +6226,8 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} + '@types/node@20.19.29': dependencies: undici-types: 6.21.0 @@ -6013,6 +6247,8 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} '@types/use-sync-external-store@0.0.6': {} @@ -6469,6 +6705,8 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 + bail@2.0.2: {} + balanced-match@1.0.2: {} baseline-browser-mapping@2.9.14: {} @@ -6533,6 +6771,10 @@ snapshots: character-entities-legacy@3.0.0: {} + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + check-error@1.0.3: dependencies: get-func-name: 2.0.2 @@ -6672,6 +6914,10 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + deep-eql@4.1.4: dependencies: type-detect: 4.1.0 @@ -6785,6 +7031,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@1.21.7)): dependencies: '@babel/core': 7.28.6 @@ -6920,6 +7168,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -6946,6 +7196,8 @@ snapshots: exsolve@1.0.8: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -7073,6 +7325,12 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -7087,6 +7345,26 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -7109,6 +7387,8 @@ snapshots: html-escaper@2.0.2: {} + html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} http-proxy-agent@7.0.2: @@ -7153,6 +7433,8 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.7: {} + input-otp@1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -7160,6 +7442,13 @@ snapshots: internmap@2.0.3: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -7168,16 +7457,22 @@ snapshots: dependencies: hasown: 2.0.2 + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-number@7.0.0: {} is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} is-stream@3.0.0: {} @@ -7291,6 +7586,8 @@ snapshots: lodash@4.17.21: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -7335,6 +7632,133 @@ snapshots: mark.js@8.11.1: {} + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -7347,29 +7771,219 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdn-data@2.12.2: {} merge-stream@2.0.0: {} merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + micromark-util-encode@2.0.1: {} + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + micromark-util-sanitize-uri@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-encode: 2.0.1 micromark-util-symbol: 2.0.1 + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-util-symbol@2.0.1: {} micromark-util-types@2.0.2: {} + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -7476,6 +8090,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse5@8.0.0: dependencies: entities: 6.0.1 @@ -7606,6 +8230,24 @@ snapshots: react-is@18.3.1: {} + react-markdown@10.1.0(@types/react@18.3.12)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.12 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-redux@9.2.0(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 @@ -7702,6 +8344,45 @@ snapshots: dependencies: regex-utilities: 2.3.0 + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + require-from-string@2.0.2: {} reselect@5.1.1: {} @@ -7844,6 +8525,14 @@ snapshots: dependencies: js-tokens: 9.0.1 + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -7955,6 +8644,8 @@ snapshots: trim-lines@3.0.1: {} + trough@2.2.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -8019,6 +8710,16 @@ snapshots: undici-types@7.16.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3