update ui
This commit is contained in:
parent
924e78d387
commit
81ff693bf7
16
README.md
16
README.md
@ -57,9 +57,23 @@ function MyComponent() {
|
|||||||
|
|
||||||
**`createUI(name, entryUrl)`** - Creates a UI component
|
**`createUI(name, entryUrl)`** - Creates a UI component
|
||||||
- `name`: Component identifier
|
- `name`: Component identifier
|
||||||
- `entryUrl`: Path from `import.meta.resolve()`
|
- `entryUrl`: Path to the component entry file
|
||||||
- Returns: `{ component(opts?) }` where opts: `{ props?, frameSize? }`
|
- Returns: `{ component(opts?) }` where opts: `{ props?, frameSize? }`
|
||||||
|
|
||||||
|
The `entryUrl` parameter accepts both formats:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ESM (recommended) - using import.meta.resolve()
|
||||||
|
// Requires "type": "module" in package.json
|
||||||
|
createUI('dashboard', import.meta.resolve('./MyComponent.tsx'));
|
||||||
|
|
||||||
|
// CommonJS - using require.resolve() or absolute paths
|
||||||
|
createUI('dashboard', require.resolve('./MyComponent.tsx'));
|
||||||
|
createUI('dashboard', path.join(__dirname, './MyComponent.tsx'));
|
||||||
|
```
|
||||||
|
|
||||||
|
The library automatically converts `file://` URLs to file paths, so both approaches work seamlessly.
|
||||||
|
|
||||||
### UI (`@mcp-ui/library/ui`)
|
### UI (`@mcp-ui/library/ui`)
|
||||||
|
|
||||||
- **`useProps(defaults)`** - Get props passed from the server
|
- **`useProps(defaults)`** - Get props passed from the server
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { generateMockStockData } from './stock-utils';
|
import { generateMockStockData } from './stock-utils';
|
||||||
import { callTool, sendPrompt, useProps } from '../../library/ui';
|
import { callTool, sendPrompt, useProps } from '@mcp-ui/library/ui';
|
||||||
|
|
||||||
|
|
||||||
// Types for props passed from the tool handler
|
// Types for props passed from the tool handler
|
||||||
@ -74,7 +74,7 @@ Please analyze this portfolio and provide recommendations.`;
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div>
|
<div>
|
||||||
<h1>📈 Stock Portfolio!!</h1>
|
<h1>📈 Stock Portfolio</h1>
|
||||||
<p className="subtitle">Timeframe: {props.timeframe} • {stocks.length} stocks</p>
|
<p className="subtitle">Timeframe: {props.timeframe} • {stocks.length} stocks</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="portfolio-summary">
|
<div className="portfolio-summary">
|
||||||
@ -2,7 +2,6 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>MCP UI Inspector</title>
|
<title>MCP UI Inspector</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -17,12 +17,13 @@
|
|||||||
.header-brand {
|
.header-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 7px;
|
||||||
color: var(--text-primary);
|
color: var(--white);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-brand svg {
|
.header-brand svg {
|
||||||
color: var(--accent-purple);
|
color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-brand h1 {
|
.header-brand h1 {
|
||||||
@ -67,8 +68,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.5; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main layout */
|
/* Main layout */
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ToolsPanel } from './components/ToolsPanel'
|
|||||||
import { ResultsPane } from './components/ResultsPane'
|
import { ResultsPane } from './components/ResultsPane'
|
||||||
import { useMCP } from './hooks/useMCP'
|
import { useMCP } from './hooks/useMCP'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
import { Eye, Sparkles } from 'lucide-react'
|
||||||
|
|
||||||
export type Tool = {
|
export type Tool = {
|
||||||
name: string
|
name: string
|
||||||
@ -43,6 +44,7 @@ function App() {
|
|||||||
const [selectedTool, setSelectedTool] = useState<Tool | null>(null)
|
const [selectedTool, setSelectedTool] = useState<Tool | null>(null)
|
||||||
const [toolResult, setToolResult] = useState<ToolResult | null>(null)
|
const [toolResult, setToolResult] = useState<ToolResult | null>(null)
|
||||||
const [isExecuting, setIsExecuting] = useState(false)
|
const [isExecuting, setIsExecuting] = useState(false)
|
||||||
|
const [lastExecution, setLastExecution] = useState<{ toolName: string; params: Record<string, unknown> } | null>(null)
|
||||||
|
|
||||||
const handleConnect = useCallback(async () => {
|
const handleConnect = useCallback(async () => {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
@ -57,6 +59,7 @@ function App() {
|
|||||||
const handleExecuteTool = useCallback(async (toolName: string, params: Record<string, unknown>) => {
|
const handleExecuteTool = useCallback(async (toolName: string, params: Record<string, unknown>) => {
|
||||||
setIsExecuting(true)
|
setIsExecuting(true)
|
||||||
setToolResult(null)
|
setToolResult(null)
|
||||||
|
setLastExecution({ toolName, params })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await callTool(toolName, params)
|
const result = await callTool(toolName, params)
|
||||||
@ -76,15 +79,16 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [callTool])
|
}, [callTool])
|
||||||
|
|
||||||
|
const handleReload = useCallback(async () => {
|
||||||
|
if (lastExecution) {
|
||||||
|
await handleExecuteTool(lastExecution.toolName, lastExecution.params)
|
||||||
|
}
|
||||||
|
}, [lastExecution, handleExecuteTool])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<div className="header-brand">
|
<div className="header-brand" onClick={() => window.location.reload()}>
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
<path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
<path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
<h1>MCP UI Inspector</h1>
|
<h1>MCP UI Inspector</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-status">
|
<div className="header-status">
|
||||||
@ -127,6 +131,7 @@ function App() {
|
|||||||
<ResultsPane
|
<ResultsPane
|
||||||
result={toolResult}
|
result={toolResult}
|
||||||
isExecuting={isExecuting}
|
isExecuting={isExecuting}
|
||||||
|
onReload={lastExecution ? handleReload : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@ -9,16 +9,17 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid var(--border-color);
|
background: var(--bg-black);
|
||||||
background: var(--bg-tertiary);
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
border: none;
|
||||||
|
outline: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover:not(:disabled) {
|
.btn:hover:not(:disabled) {
|
||||||
background: var(--bg-hover);
|
box-shadow: 0 0 10px 1px rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
@ -28,14 +29,8 @@
|
|||||||
|
|
||||||
/* Primary Variant */
|
/* Primary Variant */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--accent-blue);
|
background: var(--white);
|
||||||
border-color: var(--accent-blue);
|
color: black;
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
background: #4c9aed;
|
|
||||||
border-color: #4c9aed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ghost Variant (icon buttons) */
|
/* Ghost Variant (icon buttons) */
|
||||||
@ -45,11 +40,6 @@
|
|||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost:hover:not(:disabled) {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading State */
|
/* Loading State */
|
||||||
.btn-loading {
|
.btn-loading {
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
|
|||||||
@ -49,13 +49,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
color: var(--accent-blue);
|
color: var(--white);
|
||||||
border-bottom-color: var(--accent-blue);
|
border-bottom-color: var(--accent-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-badge {
|
.tab-badge {
|
||||||
font-size: 10px;
|
display: inline-block;
|
||||||
color: var(--accent-green);
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-badge.active {
|
||||||
|
background: var(--accent-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-actions {
|
.results-actions {
|
||||||
@ -152,7 +160,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-frame {
|
.ui-frame {
|
||||||
@ -169,7 +179,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-output pre {
|
.text-output pre {
|
||||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||||
|
monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Monitor, FileText, Clock, AlertTriangle, Maximize2, Minimize2 } from 'lucide-react'
|
import { Monitor, FileText, Clock, AlertTriangle, Maximize2, Minimize2, RotateCw } from 'lucide-react'
|
||||||
import { Button } from './Button'
|
import { Button } from './Button'
|
||||||
import type { ToolResult } from '../App'
|
import type { ToolResult } from '../App'
|
||||||
import './ResultsPane.css'
|
import './ResultsPane.css'
|
||||||
@ -7,37 +7,46 @@ import './ResultsPane.css'
|
|||||||
interface ResultsPaneProps {
|
interface ResultsPaneProps {
|
||||||
result: ToolResult | null
|
result: ToolResult | null
|
||||||
isExecuting: boolean
|
isExecuting: boolean
|
||||||
|
onReload?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = 'ui' | 'text'
|
type Tab = 'ui' | 'text'
|
||||||
|
|
||||||
export function ResultsPane({ result, isExecuting }: ResultsPaneProps) {
|
export function ResultsPane({ result, isExecuting, onReload }: ResultsPaneProps) {
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('ui')
|
const [activeTab, setActiveTab] = useState<Tab>('ui')
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
|
|
||||||
const hasUI = result?.htmlContent != null
|
const hasUI = result?.htmlContent != null
|
||||||
const hasText = result?.textContent != null
|
const hasText = result?.textContent != null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasUI) {
|
||||||
|
setActiveTab('ui')
|
||||||
|
} else if (hasText) {
|
||||||
|
setActiveTab('text')
|
||||||
|
}
|
||||||
|
}, [hasText, hasUI])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`results-pane ${isFullscreen ? 'fullscreen' : ''}`}>
|
<div className={`results-pane ${isFullscreen ? 'fullscreen' : ''}`}>
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<div className="results-tabs">
|
<div className="results-tabs">
|
||||||
<Button
|
<button
|
||||||
className={`tab ${activeTab === 'ui' ? 'active' : ''}`}
|
className={`tab ${activeTab === 'ui' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('ui')}
|
onClick={() => setActiveTab('ui')}
|
||||||
icon={<Monitor size={14} />}
|
|
||||||
>
|
>
|
||||||
UI Output
|
<Monitor size={14} />
|
||||||
{hasUI && <span className="tab-badge">✓</span>}
|
UI
|
||||||
</Button>
|
<span className={`tab-badge ${hasUI ? 'active' : ''}`} />
|
||||||
<Button
|
</button>
|
||||||
|
<button
|
||||||
className={`tab ${activeTab === 'text' ? 'active' : ''}`}
|
className={`tab ${activeTab === 'text' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('text')}
|
onClick={() => setActiveTab('text')}
|
||||||
icon={<FileText size={14} />}
|
|
||||||
>
|
>
|
||||||
Text Response
|
<FileText size={14} />
|
||||||
{hasText && <span className="tab-badge">✓</span>}
|
Text
|
||||||
</Button>
|
<span className={`tab-badge ${hasText ? 'active' : ''}`} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="results-actions">
|
<div className="results-actions">
|
||||||
@ -47,6 +56,15 @@ export function ResultsPane({ result, isExecuting }: ResultsPaneProps) {
|
|||||||
{result.timestamp.toLocaleTimeString()}
|
{result.timestamp.toLocaleTimeString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{onReload && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onReload}
|
||||||
|
disabled={isExecuting}
|
||||||
|
title="Reload"
|
||||||
|
icon={<RotateCw size={14} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||||
|
|||||||
@ -21,13 +21,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 12px 16px;
|
padding: 13px 16px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
color: var(--text-secondary);
|
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section-content {
|
.sidebar-section-content {
|
||||||
@ -99,7 +98,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-about {
|
.sidebar-about {
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export function Sidebar({
|
|||||||
{isConnected && sessionId && (
|
{isConnected && sessionId && (
|
||||||
<div className="session-info">
|
<div className="session-info">
|
||||||
<div className="session-label">Session ID</div>
|
<div className="session-label">Session ID</div>
|
||||||
<code className="session-id">{sessionId.slice(0, 8)}...</code>
|
<code className="session-id">{sessionId.slice(0, 26)}...</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-count {
|
.tool-count {
|
||||||
background: var(--bg-hover);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -165,7 +165,8 @@
|
|||||||
|
|
||||||
.json-editor {
|
.json-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||||
|
monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@ -240,7 +241,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Wrench, Play, ChevronRight, Code2, FileJson } from 'lucide-react'
|
import { Wrench, Play, Circle, CircleDot, Code2, FileJson } from 'lucide-react'
|
||||||
import { Button } from './Button'
|
import { Button } from './Button'
|
||||||
import type { Tool } from '../App'
|
import type { Tool } from '../App'
|
||||||
import './ToolsPanel.css'
|
import './ToolsPanel.css'
|
||||||
@ -148,7 +148,7 @@ export function ToolsPanel({
|
|||||||
className={`tool-item ${selectedTool?.name === tool.name ? 'selected' : ''}`}
|
className={`tool-item ${selectedTool?.name === tool.name ? 'selected' : ''}`}
|
||||||
onClick={() => onSelectTool(tool)}
|
onClick={() => onSelectTool(tool)}
|
||||||
>
|
>
|
||||||
<ChevronRight size={14} className="tool-chevron" />
|
{selectedTool?.name === tool.name ? <CircleDot size={14} className="tool-chevron" /> : <Circle size={14} className="tool-chevron" />}
|
||||||
<div className="tool-info">
|
<div className="tool-info">
|
||||||
<span className="tool-name">{tool.name}</span>
|
<span className="tool-name">{tool.name}</span>
|
||||||
{tool.description && (
|
{tool.description && (
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
--bg-primary: #000000;
|
--bg-primary: #000000;
|
||||||
--bg-secondary: #000000;
|
--bg-secondary: #000000;
|
||||||
--bg-tertiary: #141414;
|
--bg-tertiary: #141414;
|
||||||
--bg-hover: #30363d;
|
|
||||||
--border-color: #30363d;
|
--border-color: #30363d;
|
||||||
--text-primary: #e6edf3;
|
--text-primary: #e6edf3;
|
||||||
--text-secondary: #8b949e;
|
--text-secondary: #8b949e;
|
||||||
@ -18,6 +17,7 @@
|
|||||||
--accent-red: #f85149;
|
--accent-red: #f85149;
|
||||||
--accent-orange: #d29922;
|
--accent-orange: #d29922;
|
||||||
--accent-purple: #a371f7;
|
--accent-purple: #a371f7;
|
||||||
|
--white: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@ -69,51 +69,7 @@ textarea {
|
|||||||
input:focus,
|
input:focus,
|
||||||
select:focus,
|
select:focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
border-color: var(--accent-blue);
|
border-color: var(--white);
|
||||||
}
|
|
||||||
|
|
||||||
/* Button styles */
|
|
||||||
button {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s, border-color 0.2s;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary {
|
|
||||||
background: var(--accent-blue);
|
|
||||||
border-color: var(--accent-blue);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary:hover {
|
|
||||||
background: #4c9aed;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.success {
|
|
||||||
background: var(--accent-green);
|
|
||||||
border-color: var(--accent-green);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.success:hover {
|
|
||||||
background: #2ea043;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code/mono text */
|
/* Code/mono text */
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import * as esbuild from 'esbuild';
|
import * as esbuild from 'esbuild';
|
||||||
|
|
||||||
// Cache bundled JS per entry path
|
// Cache bundled JS per entry path (only used in production)
|
||||||
const bundleCache = new Map<string, string>();
|
const bundleCache = new Map<string, string>();
|
||||||
|
|
||||||
|
// In development mode, skip cache to allow hot-reloading of component changes
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
export async function bundleComponent(entryPath: string): Promise<string> {
|
export async function bundleComponent(entryPath: string): Promise<string> {
|
||||||
if (bundleCache.has(entryPath)) {
|
if (!isDev && bundleCache.has(entryPath)) {
|
||||||
return bundleCache.get(entryPath)!;
|
return bundleCache.get(entryPath)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +26,11 @@ export async function bundleComponent(entryPath: string): Promise<string> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const bundledJS = result.outputFiles[0].text;
|
const bundledJS = result.outputFiles[0].text;
|
||||||
bundleCache.set(entryPath, bundledJS);
|
|
||||||
|
// Only cache in production mode
|
||||||
|
if (!isDev) {
|
||||||
|
bundleCache.set(entryPath, bundledJS);
|
||||||
|
}
|
||||||
|
|
||||||
return bundledJS;
|
return bundledJS;
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user