integrated and initial teste unified automation

This commit is contained in:
ValueOn AG 2026-04-07 22:32:14 +02:00
parent 3a6f90a497
commit bc977b8281
28 changed files with 1287 additions and 734 deletions

View file

@ -14,13 +14,28 @@
SIDEBAR - Node List SIDEBAR - Node List
============================================================================= */ ============================================================================= */
.resizeDivider {
flex-shrink: 0;
width: 5px;
cursor: col-resize;
background: var(--border-color, #e0e0e0);
transition: background 0.15s;
position: relative;
z-index: 5;
}
.resizeDivider:hover,
.resizeDivider:active {
background: var(--primary-color, #007bff);
}
.sidebar { .sidebar {
flex-shrink: 0; flex-shrink: 0;
width: 280px; width: 280px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: var(--bg-secondary, #f8f9fa); background: var(--bg-secondary, #f8f9fa);
border-left: 1px solid var(--border-color, #e0e0e0); border-left: none;
overflow: hidden; overflow: hidden;
} }
@ -108,6 +123,7 @@
cursor: grab; cursor: grab;
transition: background 0.15s; transition: background 0.15s;
border: 1px solid transparent; border: 1px solid transparent;
position: relative;
} }
.nodeItem:hover { .nodeItem:hover {
@ -151,6 +167,29 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.nodeItem .nodeItemTooltip {
display: none;
position: absolute;
left: 0;
top: 100%;
z-index: 100;
background: var(--bg-primary, #fff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
color: var(--text-primary, #333);
white-space: normal;
word-break: break-word;
max-width: 280px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
pointer-events: none;
}
.nodeItem:hover .nodeItemTooltip {
display: block;
}
/* Loading / Error */ /* Loading / Error */
.loading, .loading,
.error { .error {
@ -318,6 +357,19 @@
box-shadow: 0 0 0 2px var(--primary-color, #007bff); box-shadow: 0 0 0 2px var(--primary-color, #007bff);
} }
.canvasNodeHighlighted {
transition: border-color 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease;
}
@keyframes pulseGlow {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.canvasNodeHighlighted[style*="box-shadow"] {
animation: pulseGlow 1.5s ease-in-out infinite;
}
.canvasNodeContent { .canvasNodeContent {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -360,6 +412,39 @@
text-decoration: underline; text-decoration: underline;
} }
.canvasNodeComment {
font-size: 0.7rem;
color: var(--text-tertiary, #999);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.2;
}
.canvasNode .canvasNodeCommentTooltip {
display: none;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: calc(100% + 6px);
z-index: 100;
background: var(--bg-primary, #fff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-size: 0.75rem;
color: var(--text-primary, #333);
white-space: normal;
word-break: break-word;
max-width: 260px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
pointer-events: none;
}
.canvasNode:hover .canvasNodeCommentTooltip {
display: block;
}
.canvasNodeInput { .canvasNodeInput {
width: 100%; width: 100%;
padding: 0.15rem 0.25rem; padding: 0.15rem 0.25rem;
@ -446,6 +531,13 @@
color: var(--text-tertiary, #999); color: var(--text-tertiary, #999);
} }
.nodeConfigDescription {
margin: -0.5rem 0 0.75rem;
font-size: 0.75rem;
color: var(--text-secondary, #666);
line-height: 1.4;
}
.nodeConfigPanel label { .nodeConfigPanel label {
display: block; display: block;
font-size: 0.75rem; font-size: 0.75rem;
@ -713,13 +805,13 @@
.scheduleModeBlock { .scheduleModeBlock {
position: relative; position: relative;
/* Ausgewählte Karte (orange) + Text auf „An“-Chips im erweiterten Bereich */ /* Ausgewählte Karte (orange) + Text auf „An“-Chips im erweiterten Bereich */
--schedule-active: var(--schedule-mode-active, var(--color-secondary, #f25843)); --schedule-active: var(--schedule-mode-active, var(--color-secondary));
--schedule-active-border: var(--schedule-mode-active-border, var(--color-text, #3a3a3a)); --schedule-active-border: var(--schedule-mode-active-border, var(--color-text));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
border-radius: 25px; border-radius: 8px;
border: 1px solid var(--color-text, #ddd); border: 1px solid var(--color-border, #E2E8F0);
background-color: var(--bg-primary, #fff); background-color: var(--bg-primary, #fff);
color: var(--color-text, #222); color: var(--color-text, #222);
overflow: hidden; overflow: hidden;
@ -1451,3 +1543,33 @@
border-color: var(--primary-color, #007bff); border-color: var(--primary-color, #007bff);
color: var(--primary-color, #007bff); color: var(--primary-color, #007bff);
} }
/* Right panel tab bar (Nodes / Tracing) */
.rightTabBar {
display: flex;
border-bottom: 1px solid var(--border-color, #e0e0e0);
flex-shrink: 0;
background: var(--bg-primary, #fff);
}
.rightTab {
flex: 1;
padding: 8px;
border: none;
background: transparent;
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #666);
transition: background 0.15s, color 0.15s;
}
.rightTab:hover {
background: var(--bg-hover, #f0f0f0);
}
.rightTabActive {
background: var(--bg-secondary, #f5f5f5);
color: var(--text-primary, #333);
box-shadow: inset 0 -2px 0 var(--primary-color, #007bff);
}

View file

@ -5,7 +5,7 @@
* Workflow configuration (gear): primary start kind + invocations; canvas start node stays in sync. * Workflow configuration (gear): primary start kind + invocations; canvas start node stays in sync.
*/ */
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { FaSpinner } from 'react-icons/fa'; import { FaSpinner } from 'react-icons/fa';
import { useApiRequest } from '../../../hooks/useApi'; import { useApiRequest } from '../../../hooks/useApi';
import { import {
@ -49,6 +49,8 @@ import { usePrompt } from '../../../hooks/usePrompt';
import { EditorChatPanel } from './EditorChatPanel'; import { EditorChatPanel } from './EditorChatPanel';
import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from './EditorChatPanel'; import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from './EditorChatPanel';
import { RunTracingPanel } from './RunTracingPanel'; import { RunTracingPanel } from './RunTracingPanel';
import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
import styles from './Automation2FlowEditor.module.css'; import styles from './Automation2FlowEditor.module.css';
const LOG = '[Automation2]'; const LOG = '[Automation2]';
@ -58,6 +60,7 @@ const DEFAULT_INVOCATIONS = (): WorkflowEntryPoint[] =>
interface Automation2FlowEditorProps { interface Automation2FlowEditorProps {
instanceId: string; instanceId: string;
mandateId?: string;
language?: string; language?: string;
/** When set, load this workflow on mount (e.g. from workflows list edit) */ /** When set, load this workflow on mount (e.g. from workflows list edit) */
initialWorkflowId?: string | null; initialWorkflowId?: string | null;
@ -65,16 +68,21 @@ interface Automation2FlowEditorProps {
onRemovePendingFile?: (fileId: string) => void; onRemovePendingFile?: (fileId: string) => void;
dataSources?: EditorDataSource[]; dataSources?: EditorDataSource[];
featureDataSources?: EditorFeatureDataSource[]; featureDataSources?: EditorFeatureDataSource[];
onFileSelect?: (fileId: string, fileName?: string) => void;
onSourcesChanged?: () => void;
} }
export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
instanceId, instanceId,
mandateId,
language = 'de', language = 'de',
initialWorkflowId, initialWorkflowId,
pendingFiles, pendingFiles,
onRemovePendingFile, onRemovePendingFile,
dataSources, dataSources,
featureDataSources, featureDataSources,
onFileSelect,
onSourcesChanged,
}) => { }) => {
const { request } = useApiRequest(); const { request } = useApiRequest();
const { prompt: promptInput, PromptDialog } = usePrompt(); const { prompt: promptInput, PromptDialog } = usePrompt();
@ -96,12 +104,64 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [invocations, setInvocations] = useState<WorkflowEntryPoint[]>(DEFAULT_INVOCATIONS); const [invocations, setInvocations] = useState<WorkflowEntryPoint[]>(DEFAULT_INVOCATIONS);
const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false); const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false);
const [chatPanelOpen, setChatPanelOpen] = useState(false); const [leftPanelOpen, setLeftPanelOpen] = useState(true);
const [tracingRunId, setTracingRunId] = useState<string | null>(null); const [tracingRunId, setTracingRunId] = useState<string | null>(null);
const [tracingNodeStatuses, setTracingNodeStatuses] = useState<Record<string, string>>({});
const [rightTab, setRightTab] = useState<'nodes' | 'tracing'>('nodes');
const [udbTab, setUdbTab] = useState<UdbTab>('chats');
const udbContext: UdbContext = useMemo(() => ({
instanceId,
mandateId: mandateId || '',
featureInstanceId: instanceId,
}), [instanceId, mandateId]);
const [versions, setVersions] = useState<AutoVersion[]>([]); const [versions, setVersions] = useState<AutoVersion[]>([]);
const [currentVersionId, setCurrentVersionId] = useState<string | null>(null); const [currentVersionId, setCurrentVersionId] = useState<string | null>(null);
const [versionLoading, setVersionLoading] = useState(false); const [versionLoading, setVersionLoading] = useState(false);
const [leftPanelWidth, setLeftPanelWidth] = useState(() => {
try { const v = parseInt(localStorage.getItem('flowEditor.leftPanelWidth') ?? ''); return v >= 240 && v <= 600 ? v : 340; } catch { return 340; }
});
const [sidebarWidth, setSidebarWidth] = useState(() => {
try { const v = parseInt(localStorage.getItem('flowEditor.sidebarWidth') ?? ''); return v >= 200 && v <= 500 ? v : 280; } catch { return 280; }
});
const resizingRef = useRef<{ target: 'left' | 'right'; startX: number; startW: number } | null>(null);
useEffect(() => {
const _onMouseMove = (e: MouseEvent) => {
if (!resizingRef.current) return;
const { target, startX, startW } = resizingRef.current;
const delta = e.clientX - startX;
if (target === 'left') {
setLeftPanelWidth(Math.max(240, Math.min(600, startW + delta)));
} else {
setSidebarWidth(Math.max(200, Math.min(500, startW - delta)));
}
};
const _onMouseUp = () => {
if (!resizingRef.current) return;
const { target } = resizingRef.current;
resizingRef.current = null;
document.body.style.cursor = '';
document.body.style.userSelect = '';
if (target === 'left') {
setLeftPanelWidth((w) => { try { localStorage.setItem('flowEditor.leftPanelWidth', String(w)); } catch {} return w; });
} else {
setSidebarWidth((w) => { try { localStorage.setItem('flowEditor.sidebarWidth', String(w)); } catch {} return w; });
}
};
document.addEventListener('mousemove', _onMouseMove);
document.addEventListener('mouseup', _onMouseUp);
return () => { document.removeEventListener('mousemove', _onMouseMove); document.removeEventListener('mouseup', _onMouseUp); };
}, []);
const _startResize = useCallback((target: 'left' | 'right', e: React.MouseEvent) => {
e.preventDefault();
resizingRef.current = { target, startX: e.clientX, startW: target === 'left' ? leftPanelWidth : sidebarWidth };
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
}, [leftPanelWidth, sidebarWidth]);
const sidebarExcludedCategories = useMemo(() => new Set(['trigger']), []); const sidebarExcludedCategories = useMemo(() => new Set(['trigger']), []);
const nodeOutputsPreview = useMemo( const nodeOutputsPreview = useMemo(
@ -149,7 +209,10 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
...(ep ? { entryPointId: ep } : {}), ...(ep ? { entryPointId: ep } : {}),
}); });
setExecuteResult(result); setExecuteResult(result);
if (result.runId) setTracingRunId(result.runId); if (result.runId) {
setTracingRunId(result.runId);
setRightTab('tracing');
}
} catch (err: unknown) { } catch (err: unknown) {
setExecuteResult({ success: false, error: err instanceof Error ? err.message : String(err) }); setExecuteResult({ success: false, error: err instanceof Error ? err.message : String(err) });
} finally { } finally {
@ -317,11 +380,13 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
loadWorkflows(); loadWorkflows();
}, [loadWorkflows]); }, [loadWorkflows]);
const lastAppliedInitialRef = useRef<string | null | undefined>(undefined);
useEffect(() => { useEffect(() => {
if (initialWorkflowId && workflows.length > 0 && !currentWorkflowId && nodeTypes.length > 0) { if (!initialWorkflowId || workflows.length === 0 || nodeTypes.length === 0) return;
if (lastAppliedInitialRef.current === initialWorkflowId) return;
lastAppliedInitialRef.current = initialWorkflowId;
handleWorkflowSelect(initialWorkflowId); handleWorkflowSelect(initialWorkflowId);
} }, [initialWorkflowId, workflows, handleWorkflowSelect, nodeTypes.length]);
}, [initialWorkflowId, workflows, currentWorkflowId, handleWorkflowSelect, nodeTypes.length]);
useEffect(() => { useEffect(() => {
if (loading || nodeTypes.length === 0) return; if (loading || nodeTypes.length === 0) return;
@ -501,10 +566,21 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
[request, instanceId, handleFromApiGraph] [request, instanceId, handleFromApiGraph]
); );
const handleWorkflowRename = useCallback(async (workflowId: string, newName: string) => {
try {
await updateWorkflow(request, instanceId, workflowId, { label: newName });
setWorkflows((prev) => prev.map((w) => w.id === workflowId ? { ...w, label: newName } : w));
} catch (e: unknown) {
console.error(`${LOG} rename failed`, e);
}
}, [request, instanceId]);
const _sidebarStyle = useMemo(() => ({ width: sidebarWidth }), [sidebarWidth]);
const renderSidebar = () => { const renderSidebar = () => {
if (loading) { if (loading) {
return ( return (
<div className={styles.sidebar}> <div className={styles.sidebar} style={_sidebarStyle}>
<div className={styles.sidebarHeader}> <div className={styles.sidebarHeader}>
<h3 className={styles.sidebarTitle}>Nodes</h3> <h3 className={styles.sidebarTitle}>Nodes</h3>
</div> </div>
@ -517,7 +593,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
} }
if (error) { if (error) {
return ( return (
<div className={styles.sidebar}> <div className={styles.sidebar} style={_sidebarStyle}>
<div className={styles.sidebarHeader}> <div className={styles.sidebarHeader}>
<h3 className={styles.sidebarTitle}>Nodes</h3> <h3 className={styles.sidebarTitle}>Nodes</h3>
</div> </div>
@ -540,6 +616,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
expandedCategories={expandedCategories} expandedCategories={expandedCategories}
onToggleCategory={toggleCategory} onToggleCategory={toggleCategory}
excludedCategories={sidebarExcludedCategories} excludedCategories={sidebarExcludedCategories}
style={_sidebarStyle}
/> />
); );
}; };
@ -552,46 +629,45 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
return ( return (
<div className={styles.container}> <div className={styles.container}>
{/* Chat/Tracing panel - left side */} {/* Left panel: Workspace (Chats / Dateien / Quellen) */}
{(chatPanelOpen || tracingRunId) && ( {leftPanelOpen && (<>
<div style={{ width: 340, borderRight: '1px solid var(--border-color, #e0e0e0)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}> <div style={{ width: leftPanelWidth, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--bg-primary, #fff)' }}>
<div style={{ display: 'flex', borderBottom: '1px solid var(--border-color, #e0e0e0)' }}> <div className={styles.rightTabBar}>
{(['chats', 'files', 'sources'] as const).map((tab) => (
<button <button
onClick={() => { setChatPanelOpen(true); setTracingRunId(null); }} key={tab}
style={{ flex: 1, padding: '8px', border: 'none', background: chatPanelOpen ? 'var(--bg-secondary, #f5f5f5)' : 'transparent', cursor: 'pointer', fontSize: '12px', fontWeight: 600 }} className={`${styles.rightTab} ${udbTab === tab ? styles.rightTabActive : ''}`}
onClick={() => setUdbTab(tab)}
> >
Chat {{ chats: 'Chats', files: 'Dateien', sources: 'Quellen' }[tab]}
</button>
<button
onClick={() => { setChatPanelOpen(false); setTracingRunId(tracingRunId || 'select'); }}
style={{ flex: 1, padding: '8px', border: 'none', background: !chatPanelOpen && tracingRunId ? 'var(--bg-secondary, #f5f5f5)' : 'transparent', cursor: 'pointer', fontSize: '12px', fontWeight: 600 }}
>
Tracing
</button>
<button
onClick={() => { setChatPanelOpen(false); setTracingRunId(null); }}
style={{ padding: '8px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '14px' }}
>
×
</button> </button>
))}
</div> </div>
<div style={{ flex: 1, overflow: 'hidden' }}> <div style={{ flex: 1, overflow: 'hidden' }}>
{chatPanelOpen && currentWorkflowId ? ( {udbTab === 'chats' ? (
<EditorChatPanel <EditorChatPanel
instanceId={instanceId} instanceId={instanceId}
workflowId={currentWorkflowId} workflowId={currentWorkflowId}
onGraphUpdated={() => handleLoad(currentWorkflowId)} onGraphUpdated={() => { if (currentWorkflowId) handleLoad(currentWorkflowId); }}
pendingFiles={pendingFiles} pendingFiles={pendingFiles}
onRemovePendingFile={onRemovePendingFile} onRemovePendingFile={onRemovePendingFile}
dataSources={dataSources} dataSources={dataSources}
featureDataSources={featureDataSources} featureDataSources={featureDataSources}
/> />
) : tracingRunId ? ( ) : (
<RunTracingPanel instanceId={instanceId} runId={tracingRunId === 'select' ? null : tracingRunId} /> <UnifiedDataBar
) : null} context={udbContext}
</div> activeTab={udbTab}
</div> onTabChange={setUdbTab}
hideTabs={['chats']}
onFileSelect={onFileSelect}
onSourcesChanged={onSourcesChanged}
/>
)} )}
</div>
</div>
<div className={styles.resizeDivider} onMouseDown={(e) => _startResize('left', e)} />
</>)}
{/* Canvas area - center */} {/* Canvas area - center */}
<div className={styles.canvas}> <div className={styles.canvas}>
@ -603,7 +679,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
onSave={handleSave} onSave={handleSave}
onExecute={handleExecute} onExecute={handleExecute}
onWorkflowSettings={() => setWorkflowSettingsOpen(true)} onWorkflowSettings={() => setWorkflowSettingsOpen(true)}
onToggleChat={() => setChatPanelOpen((prev) => !prev)} onToggleChat={() => setLeftPanelOpen((prev) => !prev)}
saving={saving} saving={saving}
executing={executing} executing={executing}
hasNodes={canvasNodes.length > 0} hasNodes={canvasNodes.length > 0}
@ -619,6 +695,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
onSaveAsTemplate={handleSaveAsTemplate} onSaveAsTemplate={handleSaveAsTemplate}
templateSaving={templateSaving} templateSaving={templateSaving}
onNewFromTemplate={() => setTemplatePickerOpen(true)} onNewFromTemplate={() => setTemplatePickerOpen(true)}
onWorkflowRename={handleWorkflowRename}
/> />
<div className={styles.canvasArea} style={{ display: 'flex', flex: 1, minWidth: 0 }}> <div className={styles.canvasArea} style={{ display: 'flex', flex: 1, minWidth: 0 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
@ -632,6 +709,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
getLabel={(node) => node.title ?? node.label ?? node.type} getLabel={(node) => node.title ?? node.label ?? node.type}
getCategoryIcon={getCategoryIcon} getCategoryIcon={getCategoryIcon}
onSelectionChange={setSelectedNode} onSelectionChange={setSelectedNode}
highlightedNodeIds={tracingRunId ? tracingNodeStatuses : undefined}
/> />
</div> </div>
{configurableSelected && selectedNode && ( {configurableSelected && selectedNode && (
@ -658,8 +736,40 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
</div> </div>
</div> </div>
{/* Node sidebar - right side */} {/* Right panel: Nodes + Tracing tabs */}
{renderSidebar()} <div className={styles.resizeDivider} onMouseDown={(e) => _startResize('right', e)} />
<div style={{ width: sidebarWidth, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--bg-secondary, #f8f9fa)' }}>
<div className={styles.rightTabBar}>
<button
className={`${styles.rightTab} ${rightTab === 'nodes' ? styles.rightTabActive : ''}`}
onClick={() => setRightTab('nodes')}
>
Nodes
</button>
<button
className={`${styles.rightTab} ${rightTab === 'tracing' ? styles.rightTabActive : ''}`}
onClick={() => { setRightTab('tracing'); if (!tracingRunId) setTracingRunId('select'); }}
>
Tracing
</button>
</div>
<div style={{ flex: 1, overflow: 'hidden' }}>
{rightTab === 'nodes' ? (
renderSidebar()
) : (
<RunTracingPanel
instanceId={instanceId}
runId={tracingRunId === 'select' ? null : tracingRunId}
onNodeSelect={(nodeId) => {
const node = canvasNodes.find((n) => n.id === nodeId);
if (node) setSelectedNode(node);
}}
onActiveStepsChange={setTracingNodeStatuses}
/>
)}
</div>
</div>
<PromptDialog /> <PromptDialog />
<WorkflowConfigurationModal <WorkflowConfigurationModal
open={workflowSettingsOpen} open={workflowSettingsOpen}

View file

@ -2,8 +2,8 @@
* CanvasHeader - Workflow controls (Neu, Speichern, laden, Ausführen), version selector, and execute result. * CanvasHeader - Workflow controls (Neu, Speichern, laden, Ausführen), version selector, and execute result.
*/ */
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect, useCallback } from 'react';
import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaRobot, FaBookmark, FaCaretDown } from 'react-icons/fa'; import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchive, FaDatabase, FaBookmark, FaCaretDown } from 'react-icons/fa';
import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi'; import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi';
import styles from './Automation2FlowEditor.module.css'; import styles from './Automation2FlowEditor.module.css';
@ -31,6 +31,7 @@ interface CanvasHeaderProps {
onSaveAsTemplate?: (scope: AutoTemplateScope) => void; onSaveAsTemplate?: (scope: AutoTemplateScope) => void;
templateSaving?: boolean; templateSaving?: boolean;
onNewFromTemplate?: () => void; onNewFromTemplate?: () => void;
onWorkflowRename?: (workflowId: string, newName: string) => void;
} }
const STATUS_BADGE: Record<string, { label: string; color: string }> = { const STATUS_BADGE: Record<string, { label: string; color: string }> = {
@ -63,6 +64,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
onSaveAsTemplate, onSaveAsTemplate,
templateSaving, templateSaving,
onNewFromTemplate, onNewFromTemplate,
onWorkflowRename,
}) => { }) => {
const currentVersion = versions?.find((v) => v.id === currentVersionId); const currentVersion = versions?.find((v) => v.id === currentVersionId);
const currentStatus = currentVersion?.status || 'draft'; const currentStatus = currentVersion?.status || 'draft';
@ -74,6 +76,34 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
const [templateMenuOpen, setTemplateMenuOpen] = useState(false); const [templateMenuOpen, setTemplateMenuOpen] = useState(false);
const templateMenuRef = useRef<HTMLDivElement>(null); const templateMenuRef = useRef<HTMLDivElement>(null);
const [editingName, setEditingName] = useState(false);
const [nameValue, setNameValue] = useState('');
const nameInputRef = useRef<HTMLInputElement>(null);
const currentWorkflow = workflows.find((w) => w.id === currentWorkflowId);
const _startNameEdit = useCallback(() => {
if (!currentWorkflowId || !onWorkflowRename) return;
setNameValue(currentWorkflow?.label || '');
setEditingName(true);
}, [currentWorkflowId, currentWorkflow?.label, onWorkflowRename]);
const _commitNameEdit = useCallback(() => {
setEditingName(false);
const trimmed = nameValue.trim();
if (!trimmed || !currentWorkflowId || !onWorkflowRename) return;
if (trimmed !== currentWorkflow?.label) {
onWorkflowRename(currentWorkflowId, trimmed);
}
}, [nameValue, currentWorkflowId, currentWorkflow?.label, onWorkflowRename]);
useEffect(() => {
if (editingName && nameInputRef.current) {
nameInputRef.current.focus();
nameInputRef.current.select();
}
}, [editingName]);
useEffect(() => { useEffect(() => {
const _handleClickOutside = (e: MouseEvent) => { const _handleClickOutside = (e: MouseEvent) => {
if (newMenuRef.current && !newMenuRef.current.contains(e.target as Node)) setNewMenuOpen(false); if (newMenuRef.current && !newMenuRef.current.contains(e.target as Node)) setNewMenuOpen(false);
@ -87,10 +117,33 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
return ( return (
<div className={styles.canvasHeader}> <div className={styles.canvasHeader}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
<h4 className={styles.canvasTitle} style={{ margin: 0 }}> {/* Workflow name: inline editable */}
Workflow-Editor {currentWorkflowId && currentWorkflow ? (
editingName ? (
<input
ref={nameInputRef}
value={nameValue}
onChange={(e) => setNameValue(e.target.value)}
onBlur={_commitNameEdit}
onKeyDown={(e) => { if (e.key === 'Enter') _commitNameEdit(); if (e.key === 'Escape') setEditingName(false); }}
style={{ padding: '0.25rem 0.4rem', fontSize: '0.95rem', fontWeight: 600, border: '1px solid var(--primary-color, #007bff)', borderRadius: 4, outline: 'none', minWidth: 140, maxWidth: 300 }}
/>
) : (
<h4
className={styles.canvasTitle}
style={{ margin: 0, cursor: onWorkflowRename ? 'pointer' : 'default', fontSize: '0.95rem', fontWeight: 600 }}
onClick={_startNameEdit}
title={onWorkflowRename ? 'Klicken zum Umbenennen' : undefined}
>
{currentWorkflow.label}
</h4> </h4>
)
) : (
<h4 className={styles.canvasTitle} style={{ margin: 0, fontStyle: 'italic', opacity: 0.6 }}>
Neuer Workflow
</h4>
)}
{onWorkflowSettings && ( {onWorkflowSettings && (
<button <button
type="button" type="button"
@ -212,9 +265,9 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
)} )}
</button> </button>
{onToggleChat && ( {onToggleChat && (
<button type="button" className={styles.retryButton} onClick={onToggleChat} title="AI Chat & Tracing öffnen"> <button type="button" className={styles.retryButton} onClick={onToggleChat} title="Workspace-Panel (Chats, Dateien, Quellen)">
<FaRobot style={{ marginRight: '0.4rem' }} /> <FaDatabase style={{ marginRight: '0.4rem' }} />
AI Chat Workspace
</button> </button>
)} )}
</div> </div>

View file

@ -44,8 +44,16 @@ interface FlowCanvasProps {
getLabel: (node: CanvasNode) => string; getLabel: (node: CanvasNode) => string;
getCategoryIcon: (category: string) => React.ReactNode; getCategoryIcon: (category: string) => React.ReactNode;
onSelectionChange?: (node: CanvasNode | null) => void; onSelectionChange?: (node: CanvasNode | null) => void;
highlightedNodeIds?: Record<string, string>;
} }
const HIGHLIGHT_COLORS: Record<string, string> = {
running: '#f0ad4e',
completed: '#28a745',
failed: '#dc3545',
skipped: '#6c757d',
};
export const FlowCanvas: React.FC<FlowCanvasProps> = ({ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
nodes, nodes,
connections, connections,
@ -56,6 +64,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
getLabel, getLabel,
getCategoryIcon, getCategoryIcon,
onSelectionChange, onSelectionChange,
highlightedNodeIds,
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [selectedNodeIds, setSelectedNodeIds] = useState<Set<string>>(new Set()); const [selectedNodeIds, setSelectedNodeIds] = useState<Set<string>>(new Set());
@ -621,18 +630,21 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
const isSelected = selectedNodeIds.has(node.id); const isSelected = selectedNodeIds.has(node.id);
const isEditingTitle = editingNodeId === node.id && editingField === 'title'; const isEditingTitle = editingNodeId === node.id && editingField === 'title';
const displayTitle = node.title ?? node.label ?? getLabel(node); const displayTitle = node.title ?? node.label ?? getLabel(node);
const hlStatus = highlightedNodeIds?.[node.id];
const hlColor = hlStatus ? HIGHLIGHT_COLORS[hlStatus] : null;
return ( return (
<div <div
key={node.id} key={node.id}
className={`${styles.canvasNode} ${isSelected ? styles.canvasNodeSelected : ''}`} className={`${styles.canvasNode} ${isSelected ? styles.canvasNodeSelected : ''} ${hlStatus ? styles.canvasNodeHighlighted : ''}`}
style={{ style={{
left: node.x, left: node.x,
top: node.y, top: node.y,
width: NODE_WIDTH, width: NODE_WIDTH,
height: NODE_HEIGHT, height: NODE_HEIGHT,
borderColor: color, borderColor: hlColor || color,
backgroundColor: `${color}15`, backgroundColor: hlColor ? `${hlColor}20` : `${color}15`,
boxShadow: hlStatus === 'running' ? `0 0 12px ${hlColor}80` : undefined,
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => { onMouseDown={(e) => {
@ -743,7 +755,13 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
{displayTitle} {displayTitle}
</span> </span>
)} )}
{node.comment && (
<span className={styles.canvasNodeComment}>{node.comment}</span>
)}
</div> </div>
{node.comment && (
<div className={styles.canvasNodeCommentTooltip}>{node.comment}</div>
)}
</div> </div>
</div> </div>
); );

View file

@ -107,6 +107,11 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({
</div> </div>
)} )}
<h4>{getLabel(nodeType?.label, language) || node.type}</h4> <h4>{getLabel(nodeType?.label, language) || node.type}</h4>
{nodeType?.description && (
<p className={styles.nodeConfigDescription}>
{getLabel(nodeType.description, language)}
</p>
)}
<ConfigRenderer <ConfigRenderer
params={params} params={params}
updateParam={updateParam} updateParam={updateParam}

View file

@ -21,7 +21,9 @@ export const NodeListItem: React.FC<NodeListItemProps> = ({
language, language,
getLabel, getLabel,
getCategoryIcon: getIcon = getCategoryIcon, getCategoryIcon: getIcon = getCategoryIcon,
}) => ( }) => {
const desc = getLabel(node.description, language);
return (
<div <div
className={styles.nodeItem} className={styles.nodeItem}
draggable draggable
@ -43,7 +45,9 @@ export const NodeListItem: React.FC<NodeListItemProps> = ({
</div> </div>
<div className={styles.nodeItemInfo}> <div className={styles.nodeItemInfo}>
<span className={styles.nodeItemLabel}>{getLabel(node.label, language)}</span> <span className={styles.nodeItemLabel}>{getLabel(node.label, language)}</span>
<span className={styles.nodeItemDesc}>{getLabel(node.description, language)}</span> <span className={styles.nodeItemDesc}>{desc}</span>
</div> </div>
{desc && <div className={styles.nodeItemTooltip}>{desc}</div>}
</div> </div>
); );
};

View file

@ -21,6 +21,7 @@ interface NodeSidebarProps {
onToggleCategory: (id: string) => void; onToggleCategory: (id: string) => void;
/** Hide palette categories (e.g. trigger — start node comes from workflow config only) */ /** Hide palette categories (e.g. trigger — start node comes from workflow config only) */
excludedCategories?: Set<string>; excludedCategories?: Set<string>;
style?: React.CSSProperties;
} }
export const NodeSidebar: React.FC<NodeSidebarProps> = ({ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
@ -32,6 +33,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
expandedCategories, expandedCategories,
onToggleCategory, onToggleCategory,
excludedCategories, excludedCategories,
style,
}) => { }) => {
const filteredNodeTypes = useMemo(() => { const filteredNodeTypes = useMemo(() => {
const visible = nodeTypes.filter( const visible = nodeTypes.filter(
@ -78,7 +80,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
getLabel(t, lang ?? language); getLabel(t, lang ?? language);
return ( return (
<div className={styles.sidebar}> <div className={styles.sidebar} style={style}>
<div className={styles.sidebarHeader}> <div className={styles.sidebarHeader}>
<h3 className={styles.sidebarTitle}>Nodes</h3> <h3 className={styles.sidebarTitle}>Nodes</h3>
<input <input

View file

@ -1,10 +1,11 @@
/** /**
* RunTracingPanel * RunTracingPanel
* *
* Shows AutoStepLog entries for a workflow run with live-update capability. * Shows AutoStepLog entries for a workflow run with live SSE push.
* Displays per-node status (running/completed/failed/skipped) with timing info. * Falls back to polling if SSE connection fails.
* Displays per-node status, timing, I/O snapshots, and retry info.
*/ */
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useApiRequest } from '../../../hooks/useApi'; import { useApiRequest } from '../../../hooks/useApi';
import type { AutoStepLog } from '../../../api/workflowApi'; import type { AutoStepLog } from '../../../api/workflowApi';
@ -12,6 +13,7 @@ interface RunTracingPanelProps {
instanceId: string; instanceId: string;
runId: string | null; runId: string | null;
onNodeSelect?: (nodeId: string) => void; onNodeSelect?: (nodeId: string) => void;
onActiveStepsChange?: (nodeStatuses: Record<string, string>) => void;
} }
const STATUS_COLORS: Record<string, string> = { const STATUS_COLORS: Record<string, string> = {
@ -30,13 +32,60 @@ const STATUS_ICONS: Record<string, string> = {
skipped: '—', skipped: '—',
}; };
function _formatTimestamp(ts: number | string | null | undefined): string {
if (!ts) return '';
const d = typeof ts === 'number' ? new Date(ts * 1000) : new Date(ts);
if (isNaN(d.getTime())) return '';
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function _truncateJson(obj: unknown, maxLen = 300): string {
if (!obj || (typeof obj === 'object' && Object.keys(obj as object).length === 0)) return '';
try {
const s = JSON.stringify(obj, null, 2);
return s.length > maxLen ? s.slice(0, maxLen) + '\n...' : s;
} catch {
return String(obj);
}
}
const CollapsibleSection: React.FC<{ label: string; content: string }> = ({ label, content }) => {
const [open, setOpen] = useState(false);
if (!content) return null;
return (
<div style={{ marginTop: '4px' }}>
<button
onClick={(e) => { e.stopPropagation(); setOpen(!open); }}
style={{
background: 'none', border: 'none', cursor: 'pointer', padding: 0,
color: 'var(--text-link, #0969da)', fontSize: '11px', textDecoration: 'underline',
}}
>
{open ? '▾' : '▸'} {label}
</button>
{open && (
<pre style={{
margin: '4px 0 0', padding: '6px', borderRadius: '4px',
background: 'var(--bg-secondary, #f6f8fa)', fontSize: '11px',
whiteSpace: 'pre-wrap', wordBreak: 'break-all', maxHeight: '200px', overflowY: 'auto',
}}>
{content}
</pre>
)}
</div>
);
};
export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
instanceId, instanceId,
runId, runId,
onNodeSelect, onNodeSelect,
onActiveStepsChange,
}) => { }) => {
const [steps, setSteps] = useState<AutoStepLog[]>([]); const [steps, setSteps] = useState<AutoStepLog[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [sseConnected, setSseConnected] = useState(false);
const eventSourceRef = useRef<EventSource | null>(null);
const { request } = useApiRequest(); const { request } = useApiRequest();
const loadSteps = useCallback(async () => { const loadSteps = useCallback(async () => {
@ -55,11 +104,65 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
} }
}, [runId, instanceId, request]); }, [runId, instanceId, request]);
// SSE live-push connection
useEffect(() => { useEffect(() => {
if (!runId || !instanceId) return;
loadSteps(); loadSteps();
const url = `/api/workflows/${instanceId}/runs/${runId}/stream`;
const es = new EventSource(url);
eventSourceRef.current = es;
es.onopen = () => setSseConnected(true);
es.onmessage = (event) => {
try {
const payload = JSON.parse(event.data);
if (payload.type === 'keepalive') return;
if (payload.type === 'run_complete' || payload.type === 'run_failed') {
loadSteps();
es.close();
setSseConnected(false);
return;
}
if (payload.status === 'running') {
setSteps((prev) => {
const exists = prev.some((s) => s.id === payload.id);
if (exists) return prev.map((s) => s.id === payload.id ? { ...s, ...payload } : s);
return [...prev, payload as AutoStepLog];
});
} else {
setSteps((prev) => prev.map((s) => s.id === payload.id ? { ...s, ...payload } : s));
}
} catch { /* ignore parse errors */ }
};
es.onerror = () => {
setSseConnected(false);
es.close();
};
return () => {
es.close();
eventSourceRef.current = null;
setSseConnected(false);
};
}, [runId, instanceId]); // eslint-disable-line react-hooks/exhaustive-deps
// Fallback polling when SSE is not connected
useEffect(() => {
if (sseConnected || !runId || !instanceId) return;
const interval = setInterval(loadSteps, 3000); const interval = setInterval(loadSteps, 3000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [loadSteps]); }, [sseConnected, runId, instanceId, loadSteps]);
// Emit active node statuses for canvas highlighting
useEffect(() => {
if (!onActiveStepsChange) return;
const nodeStatuses: Record<string, string> = {};
for (const step of steps) {
nodeStatuses[step.nodeId] = step.status;
}
onActiveStepsChange(nodeStatuses);
}, [steps, onActiveStepsChange]);
if (!runId) { if (!runId) {
return ( return (
@ -77,7 +180,14 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
{steps.length === 0 && !loading && ( {steps.length === 0 && !loading && (
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>No steps recorded yet.</div> <div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>No steps recorded yet.</div>
)} )}
{steps.map((step) => ( {steps.map((step: any) => {
const startStr = _formatTimestamp(step.startedAt);
const endStr = _formatTimestamp(step.completedAt);
const inputStr = _truncateJson(step.inputSnapshot);
const outputStr = _truncateJson(step.output);
const isLoop = step.inputSnapshot?._loopIndex != null;
return (
<div <div
key={step.id} key={step.id}
onClick={() => onNodeSelect?.(step.nodeId)} onClick={() => onNodeSelect?.(step.nodeId)}
@ -89,6 +199,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
background: 'var(--bg-primary, #fff)', background: 'var(--bg-primary, #fff)',
cursor: 'pointer', cursor: 'pointer',
fontSize: '13px', fontSize: '13px',
marginLeft: isLoop ? '16px' : '0',
}} }}
> >
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@ -98,19 +209,44 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
</span> </span>
<strong>{step.nodeType}</strong> <strong>{step.nodeType}</strong>
<span style={{ color: '#888', marginLeft: '6px' }}>({step.nodeId})</span> <span style={{ color: '#888', marginLeft: '6px' }}>({step.nodeId})</span>
{isLoop && (
<span style={{ color: '#666', marginLeft: '6px', fontSize: '11px' }}>
[iter {step.inputSnapshot._loopIndex}]
</span> </span>
)}
</span>
<span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{step.retryCount > 0 && (
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title="Retry count">
{step.retryCount}x retry
</span>
)}
{step.durationMs != null && ( {step.durationMs != null && (
<span style={{ color: '#888', fontSize: '12px' }}>{step.durationMs}ms</span> <span style={{ color: '#888', fontSize: '12px' }}>{step.durationMs}ms</span>
)} )}
</span>
</div> </div>
{(startStr || endStr) && (
<div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>
{startStr && <span>{startStr}</span>}
{startStr && endStr && <span> </span>}
{endStr && <span>{endStr}</span>}
</div>
)}
{step.error && ( {step.error && (
<div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div> <div style={{ color: '#dc3545', fontSize: '12px', marginTop: '4px' }}>{step.error}</div>
)} )}
{step.tokensUsed > 0 && ( {step.tokensUsed > 0 && (
<div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>{step.tokensUsed} tokens</div> <div style={{ color: '#888', fontSize: '11px', marginTop: '2px' }}>{step.tokensUsed} tokens</div>
)} )}
<CollapsibleSection label="Input" content={inputStr} />
<CollapsibleSection label="Output" content={outputStr} />
</div> </div>
))} );
})}
</div> </div>
); );
}; };

View file

@ -3,54 +3,57 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 6px; padding: 5px;
border: none; border: none;
border-radius: 50%; border-radius: 6px;
font-size: 12px; font-size: 12px;
font-family: var(--font-family); font-family: var(--font-family);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.15s ease;
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
min-width: 28px; min-width: 26px;
min-height: 28px; min-height: 26px;
background: var(--color-secondary); background: var(--color-gray, #718096);
color: var(--color-bg); color: #fff;
} }
.actionButton:hover { .actionButton:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover, #4A5568);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.actionButton:disabled { .actionButton:disabled {
opacity: 0.6; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
transform: none !important; transform: none !important;
box-shadow: none !important;
} }
/* Disabled state class */
.actionButton.disabled { .actionButton.disabled {
opacity: 0.4; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
transform: none !important; transform: none !important;
background: #ccc !important; background: var(--color-gray-disabled, #CBD5E0) !important;
color: #666 !important; color: var(--color-text-secondary, #718096) !important;
} }
.actionButton.disabled:hover { .actionButton.disabled:hover {
background: #ccc !important; background: var(--color-gray-disabled, #CBD5E0) !important;
transform: none !important; transform: none !important;
box-shadow: none !important;
} }
.actionButton:focus { .actionButton:focus {
outline: none; outline: none;
box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb), 0.3); box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb, 74, 111, 165), 0.25);
} }
.actionIcon { .actionIcon {
font-size: 16px; font-size: 14px;
height: 16px; height: 14px;
width: 16px; width: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -58,7 +61,7 @@
/* Loading State */ /* Loading State */
.actionButton.loading { .actionButton.loading {
opacity: 0.7; opacity: 0.65;
cursor: not-allowed; cursor: not-allowed;
} }
@ -66,7 +69,6 @@
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
/* Delete button loading state - no animation for user-friendly experience */
.actionButton.delete.loading .actionIcon { .actionButton.delete.loading .actionIcon {
animation: none; animation: none;
} }
@ -82,8 +84,8 @@
gap: 2px; gap: 2px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background: var(--color-secondary); background: var(--color-gray, #718096);
border-radius: 25px; border-radius: 6px;
padding: 2px; padding: 2px;
} }
@ -96,8 +98,8 @@
} }
.confirmButton:hover { .confirmButton:hover {
background: rgba(255, 255, 255, 0.2) !important; background: rgba(255, 255, 255, 0.15) !important;
transform: scale(1.05); transform: none;
} }
.cancelButton { .cancelButton {
@ -109,121 +111,121 @@
} }
.cancelButton:hover { .cancelButton:hover {
background: rgba(255, 255, 255, 0.2) !important; background: rgba(255, 255, 255, 0.15) !important;
transform: scale(1.05); transform: none;
} }
/* Action Button Container */ /* Action Button Container */
.actionButtons { .actionButtons {
display: flex; display: flex;
gap: 2px; gap: 4px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
} }
/* Button Variants - All use same styling as delete button */ /* Button Variants */
.actionButton.edit { .actionButton.edit {
background: var(--color-secondary); background: var(--color-secondary, #4A6FA5);
color: white; color: white;
} }
.actionButton.edit:hover { .actionButton.edit:hover {
background: var(--color-secondary-hover); background: var(--color-secondary-hover, #3D5D8A);
} }
.actionButton.delete { .actionButton.delete {
background: var(--color-secondary); background: var(--color-gray, #718096);
color: white; color: white;
} }
.actionButton.delete:hover { .actionButton.delete:hover {
background: var(--color-secondary-hover); background: var(--color-red, #C53030);
} }
.actionButton.download { .actionButton.download {
background: var(--color-secondary); background: var(--color-gray, #718096);
color: white; color: white;
} }
.actionButton.download:hover { .actionButton.download:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover, #4A5568);
} }
.actionButton.view { .actionButton.view {
background: var(--color-secondary); background: var(--color-secondary, #4A6FA5);
color: white; color: white;
} }
.actionButton.view:hover { .actionButton.view:hover {
background: var(--color-secondary-hover); background: var(--color-secondary-hover, #3D5D8A);
} }
.actionButton.copy { .actionButton.copy {
background: var(--color-secondary); background: var(--color-gray, #718096);
color: white; color: white;
} }
.actionButton.copy:hover { .actionButton.copy:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover, #4A5568);
} }
.actionButton.connect { .actionButton.connect {
background: var(--color-secondary); background: var(--color-secondary, #4A6FA5);
color: white; color: white;
} }
.actionButton.connect:hover { .actionButton.connect:hover {
background: var(--color-secondary-hover); background: var(--color-secondary-hover, #3D5D8A);
} }
.actionButton.refresh { .actionButton.refresh {
background: var(--color-secondary); background: var(--color-gray, #718096);
color: white; color: white;
} }
.actionButton.refresh:hover { .actionButton.refresh:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover, #4A5568);
} }
.actionButton.remove { .actionButton.remove {
background: var(--color-secondary); background: var(--color-gray, #718096);
color: white; color: white;
} }
.actionButton.remove:hover { .actionButton.remove:hover {
background: var(--color-secondary-hover); background: var(--color-red, #C53030);
} }
/* Generic Custom Action Button */ /* Generic Custom Action Button */
.actionButton.custom { .actionButton.custom {
background: var(--color-secondary); background: var(--color-gray, #718096);
color: white; color: white;
} }
.actionButton.custom:hover { .actionButton.custom:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover, #4A5568);
} }
/* Success State */ /* Success State */
.actionButton.success { .actionButton.success {
background: #28a745 !important; background: var(--color-success, #38A169) !important;
color: white !important; color: white !important;
} }
.actionButton.success:hover { .actionButton.success:hover {
background: #218838 !important; background: var(--color-success-hover, #2F855A) !important;
} }
/* Error State */ /* Error State */
.actionButton.error { .actionButton.error {
background: #dc3545 !important; background: var(--color-red, #C53030) !important;
color: white !important; color: white !important;
} }
.actionButton.error:hover { .actionButton.error:hover {
background: #c82333 !important; background: #9B2C2C !important;
} }
/* Responsive Design */ /* Responsive Design */
@ -234,20 +236,20 @@
} }
.actionButton { .actionButton {
padding: 4px 8px; padding: 4px;
font-size: 11px; font-size: 11px;
min-width: 24px; min-width: 22px;
min-height: 24px; min-height: 22px;
} }
.actionIcon { .actionIcon {
font-size: 14px; font-size: 13px;
height: 14px; height: 13px;
width: 14px; width: 13px;
} }
} }
/* Dark theme support - All use same styling as delete button */ /* Dark theme support */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.actionButton.edit { .actionButton.edit {
background: var(--color-secondary); background: var(--color-secondary);
@ -258,19 +260,19 @@
} }
.actionButton.delete { .actionButton.delete {
background: var(--color-secondary); background: var(--color-gray);
} }
.actionButton.delete:hover { .actionButton.delete:hover {
background: var(--color-secondary-hover); background: var(--color-red);
} }
.actionButton.download { .actionButton.download {
background: var(--color-secondary); background: var(--color-gray);
} }
.actionButton.download:hover { .actionButton.download:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover);
} }
.actionButton.view { .actionButton.view {
@ -282,11 +284,11 @@
} }
.actionButton.copy { .actionButton.copy {
background: var(--color-secondary); background: var(--color-gray);
} }
.actionButton.copy:hover { .actionButton.copy:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover);
} }
.actionButton.connect { .actionButton.connect {
@ -298,18 +300,18 @@
} }
.actionButton.refresh { .actionButton.refresh {
background: var(--color-secondary); background: var(--color-gray);
} }
.actionButton.refresh:hover { .actionButton.refresh:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover);
} }
.actionButton.custom { .actionButton.custom {
background: var(--color-secondary); background: var(--color-gray);
} }
.actionButton.custom:hover { .actionButton.custom:hover {
background: var(--color-secondary-hover); background: var(--color-gray-hover);
} }
} }

View file

@ -15,8 +15,8 @@
gap: 20px; gap: 20px;
padding: 15px; padding: 15px;
background: var(--color-bg); background: var(--color-bg);
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 8px;
} }
.searchContainer { .searchContainer {
@ -44,8 +44,8 @@
gap: 5px; gap: 5px;
height: 40px; height: 40px;
padding: 0 14px; padding: 0 14px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
background: var(--color-bg); background: var(--color-bg);
color: var(--color-text); color: var(--color-text);
font-size: 12px; font-size: 12px;
@ -58,9 +58,9 @@
} }
.csvExportButton:hover:not(:disabled) { .csvExportButton:hover:not(:disabled) {
background: var(--color-secondary); background: var(--color-gray, #718096);
color: var(--color-bg); color: #fff;
border-color: var(--color-secondary); border-color: var(--color-gray, #718096);
} }
.csvExportButton:disabled { .csvExportButton:disabled {
@ -80,21 +80,21 @@
justify-content: center; justify-content: center;
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 50%; border-radius: 6px;
background: var(--color-bg); background: var(--color-bg);
color: var(--color-text); color: var(--color-text);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.15s ease;
font-size: 16px; font-size: 16px;
font-family: var(--font-family); font-family: var(--font-family);
flex-shrink: 0; flex-shrink: 0;
} }
.refreshButton:hover:not(:disabled) { .refreshButton:hover:not(:disabled) {
background: var(--color-secondary); background: var(--color-gray, #718096);
color: white; color: white;
border-color: var(--color-secondary); border-color: var(--color-gray, #718096);
} }
.refreshButton:disabled { .refreshButton:disabled {
@ -133,7 +133,7 @@
left: 12px; left: 12px;
top: -8px; top: -8px;
transform: translateY(0); transform: translateY(0);
color: var(--color-secondary); color: var(--color-text, #2D3748);
font-size: 12px; font-size: 12px;
pointer-events: none; pointer-events: none;
transition: all 0.3s ease; transition: all 0.3s ease;
@ -147,19 +147,19 @@
width: 100%; width: 100%;
height: 40px; height: 40px;
padding: 8px 12px; padding: 8px 12px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-family: var(--font-family); font-family: var(--font-family);
background: var(--color-bg); background: var(--color-bg);
color: var(--color-text); color: var(--color-text);
transition: all 0.2s ease; transition: all 0.15s ease;
box-sizing: border-box; box-sizing: border-box;
} }
.searchInput:focus { .searchInput:focus {
border-color: var(--color-secondary); border-color: var(--color-secondary);
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1); box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb, 74, 111, 165), 0.15);
} }
.searchInput::placeholder { .searchInput::placeholder {
@ -193,15 +193,15 @@
width: 100%; width: 100%;
height: 40px; height: 40px;
padding: 6px 10px; padding: 6px 10px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-family: var(--font-family); font-family: var(--font-family);
background: var(--color-bg); background: var(--color-bg);
color: var(--color-text); color: var(--color-text);
opacity: 0.6; opacity: 0.6;
min-width: 120px; min-width: 120px;
transition: all 0.2s ease; transition: all 0.15s ease;
box-sizing: border-box; box-sizing: border-box;
} }
@ -209,7 +209,7 @@
outline: none; outline: none;
border-color: var(--color-secondary); border-color: var(--color-secondary);
opacity: 1; opacity: 1;
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1); box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb, 74, 111, 165), 0.15);
} }
.filterInput::placeholder { .filterInput::placeholder {
@ -219,8 +219,8 @@
.filterSelect { .filterSelect {
height: 40px; height: 40px;
padding: 6px 10px; padding: 6px 10px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-family: var(--font-family); font-family: var(--font-family);
background: var(--color-bg); background: var(--color-bg);
@ -239,7 +239,7 @@
/* Hide dropdown arrow when filter has a value */ /* Hide dropdown arrow when filter has a value */
.filterSelect.hasValue { .filterSelect.hasValue {
background-image: none; background-image: none;
color: var(--color-secondary); color: var(--color-text);
border-color: var(--color-secondary); border-color: var(--color-secondary);
opacity: 1; opacity: 1;
} }
@ -257,7 +257,7 @@
transform: translateY(-50%); transform: translateY(-50%);
background: none; background: none;
border: none; border: none;
color: var(--color-primary); color: var(--color-gray, #718096);
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 16px;
padding: 2px; padding: 2px;
@ -345,8 +345,8 @@
.pageSizeSelect { .pageSizeSelect {
height: 32px; height: 32px;
padding: 4px 8px; padding: 4px 8px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 4px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-family: var(--font-family); font-family: var(--font-family);
background: var(--color-bg); background: var(--color-bg);
@ -361,13 +361,13 @@
} }
.paginationButton { .paginationButton {
width: 36px; width: 30px;
height: 36px; height: 30px;
padding: 0; padding: 0;
border: none; border: 1px solid var(--color-border, #E2E8F0);
background: var(--color-secondary); background: var(--color-bg, #fff);
color: white; color: var(--color-text, #2D3748);
border-radius: 50%; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-family: var(--font-family); font-family: var(--font-family);
transition: all 0.2s ease; transition: all 0.2s ease;
@ -380,7 +380,8 @@
} }
.paginationButton:hover:not(:disabled) { .paginationButton:hover:not(:disabled) {
background: var(--color-secondary-hover); background: var(--color-gray-disabled, #EDF2F7);
border-color: var(--color-gray, #718096);
} }
.paginationButton:disabled { .paginationButton:disabled {

View file

@ -16,7 +16,7 @@
.loadingSpinner { .loadingSpinner {
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 3px solid var(--color-primary); border: 2px solid var(--color-border, #E2E8F0);
border-top-color: transparent; border-top-color: transparent;
border-radius: 50%; border-radius: 50%;
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
@ -51,10 +51,10 @@
.fieldInput { .fieldInput {
width: 100%; width: 100%;
padding: 12px 12px 8px 12px; padding: 12px 12px 8px 12px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
transition: all 0.2s ease; transition: all 0.15s ease;
background-color: var(--color-bg); background-color: var(--color-bg);
box-sizing: border-box; box-sizing: border-box;
color: var(--color-text); color: var(--color-text);
@ -79,8 +79,8 @@
.multiselectContainer { .multiselectContainer {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
background-color: var(--color-bg); background-color: var(--color-bg);
min-height: 60px; min-height: 60px;
max-height: 200px; max-height: 200px;
@ -128,7 +128,7 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
cursor: pointer; cursor: pointer;
accent-color: var(--color-primary); accent-color: var(--color-secondary);
} }
.multiselectLabel { .multiselectLabel {
@ -139,7 +139,7 @@
.multiselectCount { .multiselectCount {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--color-primary); color: var(--color-secondary);
margin-left: 4px; margin-left: 4px;
font-weight: normal; font-weight: normal;
} }
@ -148,10 +148,10 @@
.fieldTextarea { .fieldTextarea {
width: 100%; width: 100%;
padding: 12px 12px 8px 12px; padding: 12px 12px 8px 12px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
transition: all 0.2s ease; transition: all 0.15s ease;
background-color: var(--color-bg); background-color: var(--color-bg);
box-sizing: border-box; box-sizing: border-box;
color: var(--color-text); color: var(--color-text);
@ -184,7 +184,7 @@
left: 12px; left: 12px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: var(--color-primary); color: var(--color-gray, #718096);
opacity: 0.7; opacity: 0.7;
font-size: 14px; font-size: 14px;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -197,7 +197,7 @@
left: 12px; left: 12px;
top: -8px; top: -8px;
transform: translateY(0); transform: translateY(0);
color: var(--color-primary); color: var(--color-text, #2D3748);
opacity: 1; opacity: 1;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
@ -227,8 +227,8 @@
.readonlyField { .readonlyField {
padding: 12px 12px 8px 12px; padding: 12px 12px 8px 12px;
background-color: var(--color-bg); background-color: var(--color-bg);
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
color: var(--color-text); color: var(--color-text);
font-size: 14px; font-size: 14px;
min-height: 20px; min-height: 20px;
@ -252,7 +252,7 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
cursor: pointer; cursor: pointer;
accent-color: var(--color-primary); accent-color: var(--color-secondary);
} }
/* Required field indicator */ /* Required field indicator */
@ -276,15 +276,15 @@
gap: 12px; gap: 12px;
margin-top: 24px; margin-top: 24px;
padding-top: 16px; padding-top: 16px;
border-top: 1px solid var(--color-primary); border-top: 1px solid var(--color-border, #E2E8F0);
} }
.cancelButton { .cancelButton {
padding: 8px 16px; padding: 8px 16px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
background-color: var(--color-bg); background-color: var(--color-bg);
color: var(--color-text); color: var(--color-text);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
@ -307,11 +307,11 @@
border: none; border: none;
background-color: var(--color-secondary); background-color: var(--color-secondary);
color: var(--color-bg); color: var(--color-bg);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.15s ease;
} }
.submitButton:hover:not(:disabled) { .submitButton:hover:not(:disabled) {

View file

@ -4,12 +4,9 @@
gap: 10px; gap: 10px;
width: 100%; width: 100%;
font-family: var(--font-family); font-family: var(--font-family);
/* Fill available space and constrain height */
min-height: 0; min-height: 0;
flex: 1; flex: 1;
/* Prevent overflow - constrain to parent height */
overflow: hidden; overflow: hidden;
/* Ensure container respects parent's height */
height: 100%; height: 100%;
max-height: 100%; max-height: 100%;
} }
@ -22,62 +19,57 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
/* Table wrapper - contains top scrollbar and table container */ /* Table wrapper */
.tableWrapper { .tableWrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
min-height: 0; min-height: 0;
/* Constrain height to prevent growing beyond parent */
max-height: 100%; max-height: 100%;
overflow: hidden; overflow: hidden;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #e2e8f0);
border-radius: 25px; border-radius: 10px;
background: var(--color-bg); background: var(--color-bg);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
} }
/* Top horizontal scrollbar - syncs with table container */ /* Top horizontal scrollbar */
.topScrollbar { .topScrollbar {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
flex-shrink: 0; flex-shrink: 0;
background: var(--color-bg); background: var(--color-bg);
border-bottom: 1px solid var(--color-primary); border-bottom: 1px solid var(--color-border, #e2e8f0);
border-radius: 25px 25px 0 0; border-radius: 10px 10px 0 0;
} }
/* Inner div that matches table width for proper scrollbar sizing */
.topScrollbarInner { .topScrollbarInner {
height: 1px; /* Minimal height - just need width to activate scrollbar */ height: 1px;
} }
/* Table Container - scrollable area for table data only (vertical only) */ /* Table Container */
.tableContainer { .tableContainer {
position: relative; position: relative;
overflow-x: hidden; /* Horizontal scroll handled by topScrollbar */ overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-gutter: stable; scrollbar-gutter: stable;
background: var(--color-bg); background: var(--color-bg);
/* Fill remaining space but constrain to available height */
flex: 1 1 0; flex: 1 1 0;
min-height: 0; min-height: 0;
max-height: 100%; max-height: 100%;
border-radius: 0 0 25px 25px; border-radius: 0 0 10px 10px;
} }
/* Empty table styling - no extra space, just header */
.emptyTable { .emptyTable {
min-height: auto; min-height: auto;
height: auto; height: auto;
max-height: none; max-height: none;
} }
/* Hide top scrollbar when table is empty */
.emptyTable .topScrollbar { .emptyTable .topScrollbar {
display: none; display: none;
} }
/* Empty state styling */
.emptyState { .emptyState {
display: flex; display: flex;
align-items: center; align-items: center;
@ -86,13 +78,12 @@
padding: 40px 20px; padding: 40px 20px;
} }
/* Empty message styling */
.emptyMessage { .emptyMessage {
text-align: center; text-align: center;
padding: 20px; padding: 20px;
color: var(--color-text); color: var(--color-text);
opacity: 0.7; opacity: 0.5;
font-size: 1rem; font-size: 0.9rem;
line-height: 1.5; line-height: 1.5;
} }
@ -101,23 +92,21 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 40px; padding: 40px;
font-size: 16px; font-size: 14px;
color: var(--color-text); color: var(--color-text-secondary, #94a3b8);
} }
/* Table Styles */ /* Table Styles */
.table { .table {
width: 100%; width: 100%;
/* Use separate borders for sticky header support */
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
font-size: 12px; font-size: 13px;
background: var(--color-bg); background: var(--color-bg);
table-layout: fixed; table-layout: fixed;
word-wrap: break-word; word-wrap: break-word;
} }
/* Disabled user row styling */
.table tbody tr[data-user-enabled="false"] { .table tbody tr[data-user-enabled="false"] {
opacity: 0.6 !important; opacity: 0.6 !important;
background-color: rgba(0, 0, 0, 0.02) !important; background-color: rgba(0, 0, 0, 0.02) !important;
@ -128,7 +117,6 @@
background-color: rgba(0, 0, 0, 0.05) !important; background-color: rgba(0, 0, 0, 0.05) !important;
} }
/* Dark mode disabled user styling */
.dark .table tbody tr[data-user-enabled="false"] { .dark .table tbody tr[data-user-enabled="false"] {
background-color: rgba(255, 255, 255, 0.02) !important; background-color: rgba(255, 255, 255, 0.02) !important;
} }
@ -137,13 +125,11 @@
background-color: rgba(255, 255, 255, 0.05) !important; background-color: rgba(255, 255, 255, 0.05) !important;
} }
/* Sticky thead for table header */ /* Sticky thead */
.table thead { .table thead {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
/* Shadow to separate header from scrolled content */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.table thead tr { .table thead tr {
@ -151,20 +137,19 @@
} }
.th { .th {
background: var(--color-bg); background: var(--color-bg, #f8fafc);
padding: 6px 10px; padding: 10px 12px;
text-align: left; text-align: left;
font-weight: 400; font-weight: 600;
font-size: 12px; font-size: 11px;
color: var(--color-text); text-transform: uppercase;
white-space: normal; letter-spacing: 0.04em;
word-wrap: break-word; color: var(--color-text-secondary, #64748b);
overflow-wrap: break-word; white-space: nowrap;
word-break: break-word; overflow: hidden;
text-overflow: ellipsis;
user-select: none; user-select: none;
overflow: visible; border-bottom: 1px solid var(--color-border, #e2e8f0);
/* Border separates header from scrolled content */
border-bottom: 2px solid var(--color-primary);
} }
.th.actionsColumn { .th.actionsColumn {
@ -173,11 +158,12 @@
.th.sortable { .th.sortable {
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease; transition: background-color 0.15s ease, color 0.15s ease;
} }
.th.sortable:hover { .th.sortable:hover {
background: var(--color-gray-disabled); background: var(--color-gray-disabled, #f1f5f9);
color: var(--color-text, #334155);
} }
.headerContent { .headerContent {
@ -193,8 +179,8 @@
} }
.sortIcon { .sortIcon {
font-size: 12px; font-size: 11px;
color: var(--color-text-secondary, #999); color: var(--color-text-secondary, #94a3b8);
cursor: pointer; cursor: pointer;
padding: 2px; padding: 2px;
display: inline-flex; display: inline-flex;
@ -203,7 +189,7 @@
} }
.sortIcon:hover { .sortIcon:hover {
color: var(--color-secondary); color: var(--color-text, #334155);
} }
.sortIcon.sortActive { .sortIcon.sortActive {
@ -216,11 +202,11 @@
font-weight: 500; font-weight: 500;
} }
/* Filter icon in column header */ /* Filter icon */
.filterIcon { .filterIcon {
background: none; background: none;
border: none; border: none;
color: var(--color-text-secondary, #999); color: var(--color-text-secondary, #94a3b8);
cursor: pointer; cursor: pointer;
padding: 2px; padding: 2px;
display: flex; display: flex;
@ -231,13 +217,13 @@
} }
.filterIcon:hover { .filterIcon:hover {
color: var(--color-secondary); color: var(--color-text, #334155);
background: rgba(var(--color-secondary-rgb), 0.1); background: rgba(0, 0, 0, 0.05);
} }
.filterIcon.filterActive { .filterIcon.filterActive {
color: var(--color-secondary); color: var(--color-secondary);
background: rgba(var(--color-secondary-rgb), 0.15); background: rgba(var(--color-secondary-rgb), 0.1);
} }
/* Filter dropdown */ /* Filter dropdown */
@ -248,9 +234,9 @@
min-width: 180px; min-width: 180px;
max-width: 300px; max-width: 300px;
background: var(--color-bg); background: var(--color-bg);
border: 1px solid var(--color-border, #ddd); border: 1px solid var(--color-border, #e2e8f0);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
z-index: 100; z-index: 100;
margin-top: 4px; margin-top: 4px;
} }
@ -260,7 +246,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 8px 12px; padding: 8px 12px;
border-bottom: 1px solid var(--color-border, #ddd); border-bottom: 1px solid var(--color-border, #e2e8f0);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--color-text); color: var(--color-text);
@ -269,7 +255,7 @@
.filterClearBtn { .filterClearBtn {
background: none; background: none;
border: none; border: none;
color: var(--color-text-secondary); color: var(--color-text-secondary, #94a3b8);
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
padding: 2px 6px; padding: 2px 6px;
@ -277,8 +263,8 @@
} }
.filterClearBtn:hover { .filterClearBtn:hover {
background: rgba(255, 0, 0, 0.1); background: rgba(0, 0, 0, 0.06);
color: #c00; color: var(--color-text, #334155);
} }
.filterDropdownOptions { .filterDropdownOptions {
@ -298,23 +284,23 @@
} }
.filterOption:hover { .filterOption:hover {
background: var(--color-gray-disabled, #f5f5f5); background: var(--color-gray-disabled, #f1f5f9);
} }
.filterOptionSelected { .filterOptionSelected {
background: rgba(var(--color-secondary-rgb), 0.15); background: rgba(var(--color-secondary-rgb), 0.08);
color: var(--color-secondary); color: var(--color-secondary);
font-weight: 500; font-weight: 500;
} }
.filterOptionSelected:hover { .filterOptionSelected:hover {
background: rgba(var(--color-secondary-rgb), 0.2); background: rgba(var(--color-secondary-rgb), 0.12);
} }
.filterOptionMore { .filterOptionMore {
padding: 6px 12px; padding: 6px 12px;
font-size: 11px; font-size: 11px;
color: var(--color-text-secondary); color: var(--color-text-secondary, #94a3b8);
font-style: italic; font-style: italic;
} }
@ -330,21 +316,21 @@
} }
.resizeHandle:hover { .resizeHandle:hover {
background: var(--color-secondary); background: var(--color-border, #cbd5e1);
opacity: 0.5;
} }
.resizeHandle:active { .resizeHandle:active {
background: var(--color-secondary); background: var(--color-secondary);
opacity: 0.8; opacity: 0.6;
} }
/* Table cells */
.td { .td {
padding: 4px 10px; padding: 8px 12px;
border-top: 1px solid var(--color-primary); border-top: 1px solid var(--color-border, #f1f5f9);
color: var(--color-text); color: var(--color-text);
font-weight: 400; font-weight: 400;
font-size: 12px; font-size: 13px;
vertical-align: middle; vertical-align: middle;
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
@ -353,23 +339,31 @@
overflow: visible; overflow: visible;
} }
/* FK Loading state - shows truncated ID while loading */
.fkLoading { .fkLoading {
color: var(--color-text); color: var(--color-text);
opacity: 0.6; opacity: 0.6;
font-style: italic; font-style: italic;
} }
/* Rows */
.tr { .tr {
transition: background-color 0.2s ease; transition: background-color 0.12s ease;
} }
.tr:hover { .tr:hover {
background: transparent; background: var(--color-gray-disabled, #f8fafc);
}
.tr:nth-child(even) {
background: rgba(0, 0, 0, 0.015);
}
.tr:nth-child(even):hover {
background: var(--color-gray-disabled, #f8fafc);
} }
.tr.selected { .tr.selected {
background: rgba(var(--color-secondary-rgb), 0.1); background: rgba(var(--color-secondary-rgb), 0.08);
} }
.tr.clickable { .tr.clickable {
@ -384,14 +378,12 @@
position: relative; position: relative;
} }
/* Selection column header - background inherited from thead */
thead .selectColumn { thead .selectColumn {
background: var(--color-bg); background: var(--color-bg);
} }
/* Selection Column border only on body cells, not header */
tbody .selectColumn { tbody .selectColumn {
border-top: 1px solid var(--color-primary); border-top: 1px solid var(--color-border, #f1f5f9);
} }
.selectColumn input[type="checkbox"] { .selectColumn input[type="checkbox"] {
@ -402,7 +394,7 @@ tbody .selectColumn {
accent-color: var(--color-secondary); accent-color: var(--color-secondary);
margin: 0; margin: 0;
padding: 0; padding: 0;
border: 2px solid var(--color-primary); border: 1.5px solid var(--color-border, #cbd5e1);
border-radius: 3px; border-radius: 3px;
background: var(--color-bg); background: var(--color-bg);
position: relative; position: relative;
@ -426,7 +418,7 @@ tbody .selectColumn {
outline-offset: 2px; outline-offset: 2px;
} }
/* Actions Column - Fixed width like select column */ /* Actions Column */
.actionsColumn { .actionsColumn {
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: center;
@ -437,27 +429,24 @@ tbody .selectColumn {
position: relative; position: relative;
} }
/* Actions column header - background inherited from thead */
thead .actionsColumn { thead .actionsColumn {
background: var(--color-bg); background: var(--color-bg);
} }
/* Actions Column border only on body cells, not header */
tbody .actionsColumn { tbody .actionsColumn {
border-top: 1px solid var(--color-primary); border-top: 1px solid var(--color-border, #f1f5f9);
} }
.actionButtons { .actionButtons {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
gap: 2px; gap: 4px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
} }
/* Enable wrapping only when column exceeds 20% of container width */
.actionButtonsWrap { .actionButtonsWrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
@ -466,29 +455,31 @@ tbody .actionsColumn {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 4px; padding: 5px;
border: none; border: none;
border-radius: 50%; border-radius: 6px;
font-size: 11px; font-size: 11px;
font-family: var(--font-family); font-family: var(--font-family);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.15s ease;
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
min-width: 24px; min-width: 26px;
min-height: 24px; min-height: 26px;
background: var(--color-secondary); background: var(--color-text-secondary, #64748b);
color: var(--color-bg); color: #fff;
} }
.actionButton:hover { .actionButton:hover {
background: var(--color-secondary-hover); background: var(--color-text, #334155);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
} }
.actionIcon { .actionIcon {
font-size: 14px; font-size: 13px;
height: 14px; height: 13px;
width: 14px; width: 13px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -500,8 +491,8 @@ tbody .actionsColumn {
gap: 2px; gap: 2px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background: var(--color-secondary); background: var(--color-text-secondary, #64748b);
border-radius: 25px; border-radius: 8px;
} }
.confirmButton { .confirmButton {
@ -510,8 +501,8 @@ tbody .actionsColumn {
} }
.confirmButton:hover { .confirmButton:hover {
background: transparent !important; background: rgba(255, 255, 255, 0.15) !important;
transform: scale(1.05); transform: none;
} }
.cancelButton { .cancelButton {
@ -520,23 +511,24 @@ tbody .actionsColumn {
} }
.cancelButton:hover { .cancelButton:hover {
background: transparent !important; background: rgba(255, 255, 255, 0.15) !important;
transform: scale(1.05); transform: none;
} }
.actionButton:disabled { .actionButton:disabled {
opacity: 0.6; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
transform: none !important; transform: none !important;
box-shadow: none !important;
} }
/* Pagination */
.pagination { .pagination {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: 10px; gap: 8px;
padding: 8px 0; padding: 10px 0;
/* Ensure pagination stays visible and doesn't get cut off */
flex-shrink: 0; flex-shrink: 0;
flex-wrap: wrap; flex-wrap: wrap;
background: var(--color-bg); background: var(--color-bg);
@ -546,8 +538,8 @@ tbody .actionsColumn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 14px; font-size: 13px;
color: var(--color-text); color: var(--color-text-secondary, #64748b);
} }
.pageSizeSelector label { .pageSizeSelector label {
@ -556,11 +548,11 @@ tbody .actionsColumn {
} }
.pageSizeSelect { .pageSizeSelect {
height: 32px; height: 30px;
padding: 4px 8px; padding: 4px 8px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #e2e8f0);
border-radius: 4px; border-radius: 6px;
font-size: 14px; font-size: 13px;
font-family: var(--font-family); font-family: var(--font-family);
background: var(--color-bg); background: var(--color-bg);
color: var(--color-text); color: var(--color-text);
@ -571,43 +563,45 @@ tbody .actionsColumn {
.pageSizeSelect:focus { .pageSizeSelect:focus {
outline: none; outline: none;
border-color: var(--color-secondary); border-color: var(--color-secondary);
box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb), 0.15);
} }
.paginationButton { .paginationButton {
width: 36px; width: 30px;
height: 36px; height: 30px;
padding: 0; padding: 0;
border: none; border: 1px solid var(--color-border, #e2e8f0);
background: var(--color-secondary); background: var(--color-bg, #fff);
color: white; color: var(--color-text, #334155);
border-radius: 50%; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-family: var(--font-family); font-family: var(--font-family);
transition: all 0.2s ease; transition: all 0.15s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 18px; font-size: 14px;
font-weight: 500; font-weight: 500;
} }
.paginationButton:hover:not(:disabled) { .paginationButton:hover:not(:disabled) {
background: var(--color-secondary-hover); background: var(--color-gray-disabled, #f1f5f9);
border-color: var(--color-text-secondary, #94a3b8);
} }
.paginationButton:disabled { .paginationButton:disabled {
opacity: 0.5; opacity: 0.35;
cursor: not-allowed; cursor: not-allowed;
} }
.paginationInfo { .paginationInfo {
font-size: 14px; font-size: 13px;
color: var(--color-text); color: var(--color-text-secondary, #64748b);
margin: 0 15px; margin: 0 8px;
white-space: nowrap; white-space: nowrap;
} }
/* Page numbers container */ /* Page numbers */
.pageNumbers { .pageNumbers {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -617,50 +611,51 @@ tbody .actionsColumn {
max-width: 60vw; max-width: 60vw;
max-height: 120px; max-height: 120px;
overflow-y: auto; overflow-y: auto;
padding: 4px; padding: 2px;
} }
/* Individual page number button */
.pageNumber { .pageNumber {
min-width: 28px; min-width: 28px;
height: 28px; height: 28px;
padding: 0 6px; padding: 0 6px;
border: 1px solid var(--color-border, #ddd); border: 1px solid var(--color-border, #e2e8f0);
background: var(--color-bg, #fff); background: var(--color-bg, #fff);
color: var(--color-text); color: var(--color-text);
border-radius: 4px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-family: var(--font-family); font-family: var(--font-family);
font-size: 12px; font-size: 12px;
transition: all 0.15s ease; transition: all 0.12s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.pageNumber:hover:not(:disabled) { .pageNumber:hover:not(:disabled) {
background: var(--color-secondary); background: var(--color-gray-disabled, #f1f5f9);
color: white; border-color: var(--color-text-secondary, #94a3b8);
border-color: var(--color-secondary);
} }
.pageNumber:disabled { .pageNumber:disabled {
cursor: default; cursor: default;
} }
/* Active/current page number */
.pageNumberActive { .pageNumberActive {
background: var(--color-secondary); background: var(--color-text, #334155);
color: white; color: white;
border-color: var(--color-secondary); border-color: var(--color-text, #334155);
font-weight: 600; font-weight: 600;
} }
/* Ellipsis indicator */ .pageNumberActive:hover:not(:disabled) {
background: var(--color-text, #1e293b);
border-color: var(--color-text, #1e293b);
}
.pageEllipsis { .pageEllipsis {
padding: 0 8px; padding: 0 6px;
color: var(--color-text-secondary, #666); color: var(--color-text-secondary, #94a3b8);
font-size: 14px; font-size: 13px;
} }
/* Loading overlay */ /* Loading overlay */
@ -676,16 +671,16 @@ tbody .actionsColumn {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 10; z-index: 10;
border-radius: 8px; border-radius: 10px;
} }
.loadingOverlay p { .loadingOverlay p {
margin-top: 12px; margin-top: 12px;
color: var(--color-text-secondary, #666); color: var(--color-text-secondary, #64748b);
font-size: 14px; font-size: 13px;
} }
/* Responsive Design */ /* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.tableContainer { .tableContainer {
flex: 1; flex: 1;
@ -693,7 +688,6 @@ tbody .actionsColumn {
max-height: 100%; max-height: 100%;
} }
/* Empty table styling - no extra space */
.emptyTable { .emptyTable {
min-height: auto; min-height: auto;
height: auto; height: auto;
@ -701,8 +695,8 @@ tbody .actionsColumn {
.th, .th,
.td { .td {
padding: 4px 8px; padding: 6px 8px;
font-size: 11px; font-size: 12px;
} }
.actionButtons { .actionButtons {
@ -711,7 +705,7 @@ tbody .actionsColumn {
} }
.actionButton { .actionButton {
padding: 3px; padding: 4px;
font-size: 10px; font-size: 10px;
min-width: 22px; min-width: 22px;
min-height: 22px; min-height: 22px;
@ -731,7 +725,7 @@ tbody .actionsColumn {
.paginationInfo { .paginationInfo {
text-align: center; text-align: center;
margin: 0; margin: 0;
font-size: 13px; font-size: 12px;
} }
.pageNumbers { .pageNumbers {
@ -746,18 +740,22 @@ tbody .actionsColumn {
} }
} }
/* Dark theme support */ /* Dark theme */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.th.sortable:hover { .th.sortable:hover {
background: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.06);
} }
.tr:hover { .tr:hover {
background: transparent; background: rgba(255, 255, 255, 0.04);
}
.tr:nth-child(even) {
background: rgba(255, 255, 255, 0.02);
} }
.tr.selected { .tr.selected {
background: rgba(var(--color-secondary-rgb), 0.2); background: rgba(var(--color-secondary-rgb), 0.15);
} }
.loadingOverlay { .loadingOverlay {
@ -774,45 +772,43 @@ tbody .actionsColumn {
.actionButton:focus, .actionButton:focus,
.paginationButton:focus { .paginationButton:focus {
outline: none; outline: none;
box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb), 0.3);
} }
/* Custom scrollbar for table container (vertical only) */ /* Scrollbar */
.tableContainer::-webkit-scrollbar { .tableContainer::-webkit-scrollbar {
width: 8px; width: 6px;
height: 8px; height: 6px;
} }
.tableContainer::-webkit-scrollbar-track { .tableContainer::-webkit-scrollbar-track {
background: var(--color-gray-disabled); background: transparent;
border-radius: 4px;
} }
.tableContainer::-webkit-scrollbar-thumb { .tableContainer::-webkit-scrollbar-thumb {
background: var(--color-gray); background: var(--color-border, #cbd5e1);
border-radius: 4px; border-radius: 3px;
} }
.tableContainer::-webkit-scrollbar-thumb:hover { .tableContainer::-webkit-scrollbar-thumb:hover {
background: var(--color-secondary); background: var(--color-text-secondary, #94a3b8);
} }
/* Custom scrollbar for top scrollbar (horizontal only) */
.topScrollbar::-webkit-scrollbar { .topScrollbar::-webkit-scrollbar {
height: 8px; height: 6px;
} }
.topScrollbar::-webkit-scrollbar-track { .topScrollbar::-webkit-scrollbar-track {
background: var(--color-gray-disabled); background: transparent;
border-radius: 4px;
} }
.topScrollbar::-webkit-scrollbar-thumb { .topScrollbar::-webkit-scrollbar-thumb {
background: var(--color-gray); background: var(--color-border, #cbd5e1);
border-radius: 4px; border-radius: 3px;
} }
.topScrollbar::-webkit-scrollbar-thumb:hover { .topScrollbar::-webkit-scrollbar-thumb:hover {
background: var(--color-secondary); background: var(--color-text-secondary, #94a3b8);
} }
/* Loading State */ /* Loading State */
@ -823,16 +819,16 @@ tbody .actionsColumn {
justify-content: center; justify-content: center;
padding: 4rem 2rem; padding: 4rem 2rem;
text-align: center; text-align: center;
color: var(--color-text-secondary, #666); color: var(--color-text-secondary, #64748b);
} }
.loadingSpinner { .loadingSpinner {
width: 40px; width: 32px;
height: 40px; height: 32px;
border: 3px solid var(--color-bg-secondary, #e9ecef); border: 2px solid var(--color-border, #e2e8f0);
border-top: 3px solid var(--color-primary, #007bff); border-top: 2px solid var(--color-text-secondary, #64748b);
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 0.8s linear infinite;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -841,16 +837,16 @@ tbody .actionsColumn {
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* Inline Editable Boolean Cells */ /* Boolean Cells */
.booleanCell { .booleanCell {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 28px; width: 26px;
height: 28px; height: 26px;
font-size: 16px; font-size: 14px;
font-weight: bold; font-weight: bold;
border-radius: 4px; border-radius: 6px;
transition: all 0.15s ease; transition: all 0.15s ease;
user-select: none; user-select: none;
} }
@ -858,12 +854,12 @@ tbody .actionsColumn {
.booleanEditable { .booleanEditable {
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
border: 2px solid var(--color-border, #dee2e6); border: 1.5px solid var(--color-border, #e2e8f0);
} }
.booleanEditable:hover { .booleanEditable:hover {
transform: scale(1.1); transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
} }
.booleanEditable:active { .booleanEditable:active {
@ -871,25 +867,25 @@ tbody .actionsColumn {
} }
.booleanEditable.booleanTrue { .booleanEditable.booleanTrue {
color: var(--color-success, #28a745); color: var(--color-success, #16a34a);
border-color: var(--color-success, #28a745); border-color: var(--color-success, #16a34a);
background: rgba(40, 167, 69, 0.1); background: rgba(22, 163, 74, 0.08);
} }
.booleanEditable.booleanTrue:hover { .booleanEditable.booleanTrue:hover {
background: rgba(40, 167, 69, 0.2); background: rgba(22, 163, 74, 0.15);
} }
.booleanEditable.booleanFalse { .booleanEditable.booleanFalse {
color: var(--color-text-secondary, #6c757d); color: var(--color-text-secondary, #94a3b8);
border-color: var(--color-border, #dee2e6); border-color: var(--color-border, #e2e8f0);
background: transparent; background: transparent;
} }
.booleanEditable.booleanFalse:hover { .booleanEditable.booleanFalse:hover {
color: var(--color-danger, #dc3545); color: var(--color-text, #334155);
border-color: var(--color-danger, #dc3545); border-color: var(--color-text-secondary, #94a3b8);
background: rgba(220, 53, 69, 0.1); background: rgba(0, 0, 0, 0.03);
} }
.booleanReadonly { .booleanReadonly {
@ -900,11 +896,11 @@ tbody .actionsColumn {
} }
.booleanReadonly.booleanTrue { .booleanReadonly.booleanTrue {
color: var(--color-success, #28a745); color: var(--color-success, #16a34a);
} }
.booleanReadonly.booleanFalse { .booleanReadonly.booleanFalse {
color: var(--color-text-secondary, #adb5bd); color: var(--color-text-secondary, #94a3b8);
} }
.booleanLoading { .booleanLoading {
@ -912,12 +908,12 @@ tbody .actionsColumn {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
animation: booleanPulse 1s ease-in-out infinite; animation: booleanPulse 1s ease-in-out infinite;
color: var(--color-primary, #007bff); color: var(--color-text-secondary, #64748b);
font-size: 14px; font-size: 14px;
} }
@keyframes booleanPulse { @keyframes booleanPulse {
0%, 100% { opacity: 0.4; } 0%, 100% { opacity: 0.3; }
50% { opacity: 1; } 50% { opacity: 1; }
} }
@ -928,7 +924,7 @@ tbody .actionsColumn {
} }
.groupHeader:hover { .groupHeader:hover {
background-color: var(--bg-hover, #f1f5f9) !important; background-color: var(--color-gray-disabled, #f1f5f9) !important;
} }
.groupHeader .td { .groupHeader .td {
@ -943,13 +939,13 @@ tbody .actionsColumn {
width: 18px; width: 18px;
height: 18px; height: 18px;
font-size: 10px; font-size: 10px;
color: var(--text-muted, #64748b); color: var(--color-text-secondary, #64748b);
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
.groupCount { .groupCount {
font-size: 11px; font-size: 11px;
color: var(--text-muted, #64748b); color: var(--color-text-secondary, #94a3b8);
font-weight: 400; font-weight: 400;
margin-left: 4px; margin-left: 4px;
} }
@ -959,5 +955,3 @@ tbody .actionsColumn {
gap: 4px; gap: 4px;
align-items: center; align-items: center;
} }

View file

@ -9,8 +9,8 @@
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-family: var(--font-family); font-family: var(--font-family);
cursor: pointer; cursor: pointer;
@ -23,13 +23,13 @@
.dropdownButton:hover:not(.disabled):not(:disabled) { .dropdownButton:hover:not(.disabled):not(:disabled) {
border-color: var(--color-secondary); border-color: var(--color-secondary);
background-color: var(--color-secondary); background-color: var(--color-bg);
color: white; color: var(--color-text);
} }
.dropdownButton:focus { .dropdownButton:focus {
outline: none; outline: none;
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 0, 123, 255), 0.1); box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb, 74, 111, 165), 0.15);
} }
.dropdownButton.disabled, .dropdownButton.disabled,
@ -146,7 +146,7 @@
.buttonSpinner { .buttonSpinner {
width: 16px; width: 16px;
height: 16px; height: 16px;
border: 2px solid var(--color-primary); border: 2px solid var(--color-border, #E2E8F0);
border-top-color: transparent; border-top-color: transparent;
border-radius: 50%; border-radius: 50%;
animation: spin 0.6s linear infinite; animation: spin 0.6s linear infinite;
@ -164,7 +164,7 @@
left: 0; left: 0;
right: 0; right: 0;
background-color: var(--color-bg); background-color: var(--color-bg);
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000; z-index: 1000;
@ -178,7 +178,7 @@
font-weight: 600; font-weight: 600;
color: var(--color-text); color: var(--color-text);
background-color: var(--color-bg); background-color: var(--color-bg);
border-bottom: 1px solid var(--color-primary); border-bottom: 1px solid var(--color-border, #E2E8F0);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
@ -205,7 +205,7 @@
cursor: pointer; cursor: pointer;
font-family: var(--font-family); font-family: var(--font-family);
transition: all 0.2s ease; transition: all 0.2s ease;
border-bottom: 1px solid var(--color-primary); border-bottom: 1px solid var(--color-border, #E2E8F0);
color: var(--color-text); color: var(--color-text);
display: flex; display: flex;
align-items: center; align-items: center;
@ -218,19 +218,18 @@
} }
.dropdownItem:hover { .dropdownItem:hover {
background-color: var(--color-secondary); background-color: var(--color-highlight-gray, #F7FAFC);
color: white; color: var(--color-text);
} }
.dropdownItemSelected { .dropdownItemSelected {
background-color: var(--color-primary); background-color: rgba(var(--color-secondary-rgb, 74, 111, 165), 0.1);
color: white; color: var(--color-secondary);
font-weight: 500; font-weight: 500;
} }
.dropdownItemSelected:hover { .dropdownItemSelected:hover {
background-color: var(--color-primary); background-color: rgba(var(--color-secondary-rgb, 74, 111, 165), 0.15);
opacity: 0.9;
} }
.itemLabel { .itemLabel {

View file

@ -15,10 +15,10 @@
/* Main popup container */ /* Main popup container */
.popup { .popup {
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
background: var(--color-bg); background: var(--color-bg);
border-radius: 25px; border-radius: 10px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
max-height: 90vh; max-height: 90vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -64,7 +64,7 @@
/* Header section */ /* Header section */
.header { .header {
padding: 20px 24px 16px; padding: 20px 24px 16px;
border-bottom: 1px solid var(--color-primary); border-bottom: 1px solid var(--color-border, #E2E8F0);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -101,13 +101,13 @@
gap: 6px; gap: 6px;
padding: 15px; padding: 15px;
border: none; border: none;
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.15s ease;
white-space: nowrap; white-space: nowrap;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: var(--font-family);
background: var(--color-secondary); background: var(--color-secondary);
color: white; color: white;
} }
@ -140,13 +140,13 @@
} }
.closeButton { .closeButton {
background: var(--color-primary); background: var(--color-gray-disabled, #CBD5E0);
border: none; border: none;
font-size: 18px; font-size: 18px;
color: #3A3A3A; color: var(--color-text, #2D3748);
cursor: pointer; cursor: pointer;
padding: 8px; padding: 8px;
border-radius: 25px; border-radius: 6px;
transition: all 0.2s ease; transition: all 0.2s ease;
line-height: 1; line-height: 1;
margin-left: 8px; margin-left: 8px;
@ -158,8 +158,8 @@
} }
.closeButton:hover { .closeButton:hover {
background-color: var(--color-primary-hover); background-color: var(--color-gray, #718096);
color: #3A3A3A; color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
@ -173,7 +173,7 @@
/* Footer section */ /* Footer section */
.footer { .footer {
padding: 16px 24px; padding: 16px 24px;
border-top: 1px solid var(--color-primary); border-top: 1px solid var(--color-border, #E2E8F0);
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 12px; gap: 12px;

View file

@ -30,8 +30,8 @@
.textarea { .textarea {
width: 100%; width: 100%;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid var(--color-primary); border: 1px solid var(--color-border, #E2E8F0);
border-radius: 25px; border-radius: 6px;
font-size: 14px; font-size: 14px;
font-family: inherit; font-family: inherit;
color: var(--color-text); color: var(--color-text);
@ -54,7 +54,7 @@
.textarea:focus { .textarea:focus {
outline: none; outline: none;
border-color: var(--color-secondary); border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 0, 123, 255), 0.1); box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb, 74, 111, 165), 0.15);
} }
.input:disabled, .input:disabled,

View file

@ -221,6 +221,9 @@ export const FeatureViewPage: React.FC<FeatureViewPageProps> = ({ view }) => {
return null; return null;
} }
// GraphicalEditor sub-pages have their own headers with actions; skip the wrapper title.
const _skipViewHeader = featureCode === 'graphicalEditor';
// View-Komponente finden // View-Komponente finden
const featureViews = VIEW_COMPONENTS[featureCode]; const featureViews = VIEW_COMPONENTS[featureCode];
if (!featureViews) { if (!featureViews) {
@ -240,9 +243,11 @@ export const FeatureViewPage: React.FC<FeatureViewPageProps> = ({ view }) => {
return ( return (
<div className={styles.featureView}> <div className={styles.featureView}>
{!_skipViewHeader && (
<header className={styles.viewHeader}> <header className={styles.viewHeader}>
<h1 className={styles.viewTitle}>{viewLabel}</h1> <h1 className={styles.viewTitle}>{viewLabel}</h1>
</header> </header>
)}
<main className={styles.viewContent}> <main className={styles.viewContent}>
<ViewComponent /> <ViewComponent />
</main> </main>

View file

@ -47,8 +47,8 @@
margin-top: 2rem; margin-top: 2rem;
padding: 2rem; padding: 2rem;
border-radius: 25px; border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent); border: 1px solid var(--color-border, #E2E8F0);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1); 0 0 10px rgba(0, 0, 0, 0.1);
} }
@ -73,7 +73,7 @@
left: 16px; left: 16px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: var(--color-primary); color: var(--color-gray, #718096);
font-size: 1rem; font-size: 1rem;
pointer-events: none; pointer-events: none;
transition: all 0.3s ease; transition: all 0.3s ease;
@ -101,10 +101,10 @@
height: 50px; height: 50px;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid var(--color-gray-disabled); border: 1px solid var(--color-gray-disabled);
border-radius: 25px; border-radius: 6px;
font-size: 1rem; font-size: 1rem;
transition: all 0.2s ease; transition: all 0.15s ease;
background-color: var(--color-bg); background-color: var(--color-bg);
color: var(--color-text); color: var(--color-text);
font-family: var(--font-family); font-family: var(--font-family);
@ -147,7 +147,7 @@
width: 100%; width: 100%;
height: 50px; height: 50px;
padding: 12px 20px; padding: 12px 20px;
border-radius: 25px; border-radius: 6px;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
@ -171,7 +171,7 @@
.loginButton { .loginButton {
background-color: var(--color-secondary); background-color: var(--color-secondary);
color: var(--color-text); color: #fff;
} }
.loginButton:hover { .loginButton:hover {
@ -179,21 +179,23 @@
} }
.microsoftButton { .microsoftButton {
background-color: var(--color-primary); background-color: var(--color-gray-disabled, #CBD5E0);
color: var(--color-bg); color: var(--color-text);
} }
.microsoftButton:hover { .microsoftButton:hover {
background-color: var(--color-primary-hover); background-color: var(--color-gray, #718096);
color: #fff;
} }
.googleButton { .googleButton {
background-color: var(--color-primary); background-color: var(--color-gray-disabled, #CBD5E0);
color: var(--color-bg); color: var(--color-text);
} }
.googleButton:hover { .googleButton:hover {
background-color: var(--color-primary-hover); background-color: var(--color-gray, #718096);
color: #fff;
} }
.divider { .divider {
@ -252,13 +254,13 @@
flex: 1; flex: 1;
height: 46px; height: 46px;
padding: 10px 16px; padding: 10px 16px;
border-radius: 25px; border-radius: 6px;
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
border: none; border: none;
background-color: var(--color-secondary); background-color: var(--color-secondary);
color: var(--color-text); color: #fff;
transition: all 0.2s ease; transition: all 0.2s ease;
font-family: var(--font-family); font-family: var(--font-family);
} }
@ -271,7 +273,7 @@
flex: 1; flex: 1;
height: 46px; height: 46px;
padding: 10px 16px; padding: 10px 16px;
border-radius: 25px; border-radius: 6px;
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
@ -349,7 +351,7 @@ button:disabled {
width: 100%; width: 100%;
margin-top: 1.25rem; margin-top: 1.25rem;
padding: 1.25rem; padding: 1.25rem;
border-radius: 20px; border-radius: 10px;
} }
.registerLink { .registerLink {
@ -365,7 +367,7 @@ button:disabled {
.loginBox { .loginBox {
padding: 1rem; padding: 1rem;
border-radius: 16px; border-radius: 10px;
} }
.input, .input,

View file

@ -47,8 +47,8 @@
margin-top: 2rem; margin-top: 2rem;
padding: 2rem; padding: 2rem;
border-radius: 25px; border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent); border: 1px solid var(--color-border, #E2E8F0);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1); 0 0 10px rgba(0, 0, 0, 0.1);
} }
@ -105,7 +105,7 @@
height: 50px; height: 50px;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid var(--color-gray-disabled); border: 1px solid var(--color-gray-disabled);
border-radius: 25px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -145,7 +145,7 @@
width: 100%; width: 100%;
height: 50px; height: 50px;
padding: 12px 20px; padding: 12px 20px;
border-radius: 25px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
@ -200,7 +200,7 @@ button:disabled {
color: var(--color-secondary); color: var(--color-secondary);
background-color: var(--color-secondary-disabled); background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: 25px; border-radius: 8px;
padding: 12px; padding: 12px;
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;
@ -212,7 +212,7 @@ button:disabled {
color: var(--color-success); color: var(--color-success);
background-color: color-mix(in srgb, var(--color-success) 10%, transparent); background-color: color-mix(in srgb, var(--color-success) 10%, transparent);
border: 1px solid var(--color-success); border: 1px solid var(--color-success);
border-radius: 25px; border-radius: 8px;
padding: 12px; padding: 12px;
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;

View file

@ -47,8 +47,8 @@
margin-top: 2rem; margin-top: 2rem;
padding: 2rem; padding: 2rem;
border-radius: 25px; border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent); border: 1px solid var(--color-border, #E2E8F0);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1); 0 0 10px rgba(0, 0, 0, 0.1);
} }
@ -101,7 +101,7 @@
height: 50px; height: 50px;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid var(--color-gray-disabled); border: 1px solid var(--color-gray-disabled);
border-radius: 25px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -152,7 +152,7 @@
width: 100%; width: 100%;
height: 50px; height: 50px;
padding: 12px 20px; padding: 12px 20px;
border-radius: 25px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
@ -214,7 +214,7 @@ button:disabled {
color: var(--color-secondary); color: var(--color-secondary);
background-color: var(--color-secondary-disabled); background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: 25px; border-radius: 8px;
padding: 12px; padding: 12px;
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;
@ -226,7 +226,7 @@ button:disabled {
color: var(--color-success); color: var(--color-success);
background-color: color-mix(in srgb, var(--color-success) 10%, transparent); background-color: color-mix(in srgb, var(--color-success) 10%, transparent);
border: 1px solid var(--color-success); border: 1px solid var(--color-success);
border-radius: 25px; border-radius: 8px;
padding: 12px; padding: 12px;
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;

View file

@ -47,8 +47,8 @@
margin-top: 2rem; margin-top: 2rem;
padding: 2rem; padding: 2rem;
border-radius: 25px; border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent); border: 1px solid var(--color-border, #E2E8F0);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02), box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1); 0 0 10px rgba(0, 0, 0, 0.1);
} }
@ -106,7 +106,7 @@
height: 50px; height: 50px;
padding: 12px 16px; padding: 12px 16px;
border: 1px solid var(--color-gray-disabled); border: 1px solid var(--color-gray-disabled);
border-radius: 25px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -153,7 +153,7 @@
width: 100%; width: 100%;
height: 50px; height: 50px;
padding: 12px 20px; padding: 12px 20px;
border-radius: 25px; border-radius: 8px;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
@ -209,7 +209,7 @@ button:disabled {
color: var(--color-secondary); color: var(--color-secondary);
background-color: var(--color-secondary-disabled); background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary); border: 1px solid var(--color-secondary);
border-radius: 25px; border-radius: 8px;
padding: 12px; padding: 12px;
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;
@ -221,7 +221,7 @@ button:disabled {
color: var(--color-success); color: var(--color-success);
background-color: color-mix(in srgb, var(--color-success) 10%, transparent); background-color: color-mix(in srgb, var(--color-success) 10%, transparent);
border: 1px solid var(--color-success); border: 1px solid var(--color-success);
border-radius: 25px; border-radius: 8px;
padding: 12px; padding: 12px;
font-size: 0.9rem; font-size: 0.9rem;
text-align: center; text-align: center;

View file

@ -6,7 +6,7 @@
*/ */
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { FaSync, FaPlay, FaCog, FaClipboardList, FaChartBar } from 'react-icons/fa'; import { FaSync, FaPlay, FaCog, FaClipboardList, FaChartBar, FaDownload } from 'react-icons/fa';
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator'; import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator';
import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useApiRequest } from '../../../hooks/useApi'; import { useApiRequest } from '../../../hooks/useApi';
@ -110,6 +110,36 @@ export const GraphicalEditorDashboardPage: React.FC = () => {
load(); load();
}, [load]); }, [load]);
const _downloadRunTracing = useCallback(async (run: CompletedRun) => {
if (!instanceId || !run.id) return;
try {
const data = await request({
url: `/api/workflows/${instanceId}/runs/${run.id}/steps`,
method: 'get',
});
const steps = data?.steps || [];
const report = {
runId: run.id,
workflowId: run.workflowId,
workflowLabel: run.workflowLabel,
status: run.status,
startedAt: _formatTs(run.sysCreatedAt),
endedAt: _formatTs(run.sysModifiedAt),
steps,
};
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `run-tracing-${run.id.slice(0, 8)}.json`;
a.click();
URL.revokeObjectURL(url);
} catch (e) {
console.error('[dashboard] download tracing failed', e);
showError('Download fehlgeschlagen');
}
}, [instanceId, request, showError]);
const runColumns: ColumnConfig[] = [ const runColumns: ColumnConfig[] = [
{ {
key: 'workflowLabel', key: 'workflowLabel',
@ -144,6 +174,26 @@ export const GraphicalEditorDashboardPage: React.FC = () => {
width: 150, width: 150,
formatter: (v: number) => _formatTs(v), formatter: (v: number) => _formatTs(v),
}, },
{
key: 'id',
label: '',
type: 'string',
width: 50,
sortable: false,
formatter: (_v: string, row: CompletedRun) => (
<button
onClick={(e) => { e.stopPropagation(); _downloadRunTracing(row); }}
title="Tracing-Protokoll herunterladen"
style={{
border: 'none', background: 'transparent', cursor: 'pointer',
color: 'var(--text-secondary, #666)', fontSize: 14, padding: 4,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
<FaDownload />
</button>
),
},
]; ];
if (!instanceId) { if (!instanceId) {

View file

@ -1,25 +1,16 @@
/** /**
* GraphicalEditorPage * GraphicalEditorPage
* *
* Layout: [UDB sidebar (collapsible)] [FlowEditor (flex)] [Chat/Tracing (inside FlowEditor)] * Thin wrapper: passes instance context to FlowEditor which now owns the full layout
* UDB provides access to Files & Sources while configuring nodes. * including the Workspace panel (Chats/Dateien/Quellen) on the left.
* AI Chat and Tracing panels are managed by the FlowEditor's CanvasHeader.
*
* File/Source attachment UX mirrors the Workspace:
* - Files: click in UDB FilesTab added as pendingFile chip in chat input
* - Data Sources: 🔗 picker button in chat input (loaded from UDB API)
*/ */
import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { FaDatabase, FaChevronLeft } from 'react-icons/fa';
import { useInstanceId, useMandateId } from '../../../hooks/useCurrentInstance'; import { useInstanceId, useMandateId } from '../../../hooks/useCurrentInstance';
import { useLanguage } from '../../../providers/language/LanguageContext'; import { useLanguage } from '../../../providers/language/LanguageContext';
import { Automation2FlowEditor as FlowEditor } from '../../../components/FlowEditor'; import { Automation2FlowEditor as FlowEditor } from '../../../components/FlowEditor';
import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from '../../../components/FlowEditor'; import type { PendingFile, EditorDataSource, EditorFeatureDataSource } from '../../../components/FlowEditor';
import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
import api from '../../../api'; import api from '../../../api';
import styles from '../../FeatureView.module.css';
interface GraphicalEditorPageProps { interface GraphicalEditorPageProps {
persistentInstanceId?: string; persistentInstanceId?: string;
@ -35,22 +26,24 @@ export const GraphicalEditorPage: React.FC<GraphicalEditorPageProps> = ({
const instanceId = persistentInstanceId || urlInstanceId; const instanceId = persistentInstanceId || urlInstanceId;
const mandateId = persistentMandateId || urlMandateId; const mandateId = persistentMandateId || urlMandateId;
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const initialWorkflowIdRef = useRef(searchParams.get('workflowId')); const workflowIdFromUrl = searchParams.get('workflowId');
const [activeWorkflowId, setActiveWorkflowId] = useState<string | null>(workflowIdFromUrl);
const prevWorkflowIdRef = useRef(workflowIdFromUrl);
useEffect(() => {
if (workflowIdFromUrl && workflowIdFromUrl !== prevWorkflowIdRef.current) {
prevWorkflowIdRef.current = workflowIdFromUrl;
setActiveWorkflowId(workflowIdFromUrl);
}
}, [workflowIdFromUrl]);
const { currentLanguage } = useLanguage(); const { currentLanguage } = useLanguage();
const language = (currentLanguage?.slice(0, 2) || 'de') as string; const language = (currentLanguage?.slice(0, 2) || 'de') as string;
const [udbTab, setUdbTab] = useState<UdbTab>('files');
const [udbOpen, setUdbOpen] = useState(true);
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]); const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
const [dataSources, setDataSources] = useState<EditorDataSource[]>([]); const [dataSources, setDataSources] = useState<EditorDataSource[]>([]);
const [featureDataSources, setFeatureDataSources] = useState<EditorFeatureDataSource[]>([]); const [featureDataSources, setFeatureDataSources] = useState<EditorFeatureDataSource[]>([]);
const udbContext: UdbContext = useMemo(() => ({
instanceId: instanceId || '',
mandateId: mandateId || '',
featureInstanceId: instanceId || '',
}), [instanceId, mandateId]);
useEffect(() => { useEffect(() => {
if (!instanceId) return; if (!instanceId) return;
api.get(`/api/workspace/${instanceId}/datasources`) api.get(`/api/workspace/${instanceId}/datasources`)
@ -108,7 +101,7 @@ export const GraphicalEditorPage: React.FC<GraphicalEditorPageProps> = ({
if (!instanceId) { if (!instanceId) {
return ( return (
<div className={styles.placeholder}> <div style={{ padding: '2rem', textAlign: 'center' }}>
<h2>Graphical Editor</h2> <h2>Graphical Editor</h2>
<p>Keine Feature-Instanz gefunden.</p> <p>Keine Feature-Instanz gefunden.</p>
</div> </div>
@ -116,75 +109,19 @@ export const GraphicalEditorPage: React.FC<GraphicalEditorPageProps> = ({
} }
return ( return (
<div style={{ flex: 1, minHeight: 0, display: 'flex', position: 'relative' }}> <div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
{/* UDB Sidebar */}
{udbOpen ? (
<div style={{
width: 280, minWidth: 280,
borderRight: '1px solid var(--border-color, #e0e0e0)',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
background: 'var(--bg-primary, #fff)',
}}>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '6px 12px',
borderBottom: '1px solid var(--border-color, #e0e0e0)',
background: 'var(--bg-secondary, #f8f9fa)',
}}>
<span style={{ fontWeight: 600, fontSize: '13px', display: 'flex', alignItems: 'center', gap: 6 }}>
<FaDatabase style={{ fontSize: 12, opacity: 0.6 }} /> Daten
</span>
<button
onClick={() => setUdbOpen(false)}
title="Sidebar schliessen"
style={{
border: 'none', background: 'transparent', cursor: 'pointer',
fontSize: '14px', padding: '2px 4px', borderRadius: 4,
display: 'flex', alignItems: 'center',
}}
>
<FaChevronLeft style={{ fontSize: 12 }} />
</button>
</div>
<UnifiedDataBar
context={udbContext}
activeTab={udbTab}
onTabChange={setUdbTab}
hideTabs={['chats']}
onFileSelect={_handleFileSelect}
onSourcesChanged={_handleSourcesChanged}
/>
</div>
) : (
<button
onClick={() => setUdbOpen(true)}
title="Daten-Sidebar öffnen"
style={{
position: 'absolute', left: 0, top: '50%', transform: 'translateY(-50%)',
zIndex: 20, width: 28, height: 80,
border: '1px solid var(--border-color, #ddd)', borderLeft: 'none',
borderRadius: '0 8px 8px 0',
background: 'var(--bg-primary, #fff)', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '2px 0 6px rgba(0,0,0,0.06)',
}}
>
<FaDatabase style={{ fontSize: 13, opacity: 0.5 }} />
</button>
)}
{/* FlowEditor */}
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
<FlowEditor <FlowEditor
instanceId={instanceId} instanceId={instanceId}
mandateId={mandateId || undefined}
language={language} language={language}
initialWorkflowId={initialWorkflowIdRef.current} initialWorkflowId={activeWorkflowId}
pendingFiles={pendingFiles} pendingFiles={pendingFiles}
onRemovePendingFile={_handleRemovePendingFile} onRemovePendingFile={_handleRemovePendingFile}
dataSources={dataSources} dataSources={dataSources}
featureDataSources={featureDataSources} featureDataSources={featureDataSources}
onFileSelect={_handleFileSelect}
onSourcesChanged={_handleSourcesChanged}
/> />
</div> </div>
</div>
); );
}; };

View file

@ -8,7 +8,8 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { FaCopy, FaSync, FaShareAlt } from 'react-icons/fa'; import { FaCopy, FaSync, FaShareAlt, FaPen } from 'react-icons/fa';
import { usePrompt } from '../../../hooks/usePrompt';
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator'; import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator';
import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useApiRequest } from '../../../hooks/useApi'; import { useApiRequest } from '../../../hooks/useApi';
@ -17,6 +18,7 @@ import {
copyTemplate, copyTemplate,
shareTemplate, shareTemplate,
deleteWorkflow, deleteWorkflow,
updateWorkflow,
type AutoWorkflowTemplate, type AutoWorkflowTemplate,
type AutoTemplateScope, type AutoTemplateScope,
} from '../../../api/workflowApi'; } from '../../../api/workflowApi';
@ -50,6 +52,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
const { request } = useApiRequest(); const { request } = useApiRequest();
const navigate = useNavigate(); const navigate = useNavigate();
const { showSuccess, showError } = useToast(); const { showSuccess, showError } = useToast();
const { prompt: promptInput, PromptDialog } = usePrompt();
const [templates, setTemplates] = useState<AutoWorkflowTemplate[]>([]); const [templates, setTemplates] = useState<AutoWorkflowTemplate[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -117,18 +120,15 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
); );
const handleShare = useCallback( const handleShare = useCallback(
async (row: AutoWorkflowTemplate) => { async (row: AutoWorkflowTemplate, targetScope: AutoTemplateScope) => {
if (!instanceId) return; if (!instanceId) return;
const currentScope = row.templateScope || 'user';
const nextScope: AutoTemplateScope =
currentScope === 'user' ? 'instance' : currentScope === 'instance' ? 'mandate' : 'mandate';
setSharingId(row.id); setSharingId(row.id);
try { try {
await shareTemplate(request, instanceId, row.id, nextScope); await shareTemplate(request, instanceId, row.id, targetScope);
showSuccess(`Vorlage freigegeben (Scope: ${SCOPE_LABELS[nextScope]})`); showSuccess(`Scope geändert: ${SCOPE_LABELS[targetScope]}`);
await load(); await load();
} catch (e: any) { } catch (e: any) {
showError(`Fehler: ${e?.message || 'Freigabe fehlgeschlagen'}`); showError(`Fehler: ${e?.message || 'Scope-Änderung fehlgeschlagen'}`);
} finally { } finally {
setSharingId(null); setSharingId(null);
} }
@ -136,6 +136,28 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
[instanceId, request, showSuccess, showError, load] [instanceId, request, showSuccess, showError, load]
); );
const [scopeMenuId, setScopeMenuId] = useState<string | null>(null);
const handleRename = useCallback(
async (row: AutoWorkflowTemplate) => {
if (!instanceId) return;
const newLabel = await promptInput('Neuer Name:', {
title: 'Vorlage umbenennen',
defaultValue: row.label,
placeholder: 'Vorlagen-Name',
});
if (!newLabel || newLabel.trim() === row.label) return;
try {
await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() });
showSuccess('Vorlage umbenannt');
await load();
} catch (e: any) {
showError(`Fehler: ${e?.message || 'Umbenennen fehlgeschlagen'}`);
}
},
[instanceId, request, promptInput, showSuccess, showError, load]
);
const handleEdit = useCallback( const handleEdit = useCallback(
(row: AutoWorkflowTemplate) => { (row: AutoWorkflowTemplate) => {
if (!mandateId || !instanceId) return; if (!mandateId || !instanceId) return;
@ -243,6 +265,13 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
}, },
]} ]}
customActions={[ customActions={[
{
id: 'rename',
icon: <FaPen />,
title: 'Umbenennen',
onClick: (row) => handleRename(row),
visible: (row) => (row.templateScope || 'user') !== 'system',
},
{ {
id: 'copy', id: 'copy',
icon: <FaCopy />, icon: <FaCopy />,
@ -251,10 +280,10 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
loading: (row) => copyingId === row.id, loading: (row) => copyingId === row.id,
}, },
{ {
id: 'share', id: 'scope',
icon: <FaShareAlt />, icon: <FaShareAlt />,
title: 'Scope erweitern (freigeben)', title: 'Scope ändern',
onClick: (row) => handleShare(row), onClick: (row) => setScopeMenuId(scopeMenuId === row.id ? null : row.id),
loading: (row) => sharingId === row.id, loading: (row) => sharingId === row.id,
visible: (row) => (row.templateScope || 'user') !== 'system', visible: (row) => (row.templateScope || 'user') !== 'system',
}, },
@ -264,6 +293,59 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
emptyMessage="Keine Vorlagen gefunden. Erstelle eine Vorlage aus einem bestehenden Workflow." emptyMessage="Keine Vorlagen gefunden. Erstelle eine Vorlage aus einem bestehenden Workflow."
/> />
</div> </div>
{/* Scope change dropdown overlay */}
{scopeMenuId && (() => {
const tpl = templates.find(t => t.id === scopeMenuId);
if (!tpl) return null;
const currentScope = (tpl.templateScope || 'user') as AutoTemplateScope;
const scopes: AutoTemplateScope[] = ['user', 'instance', 'mandate'];
return (
<div
style={{ position: 'fixed', inset: 0, zIndex: 1000 }}
onClick={() => setScopeMenuId(null)}
>
<div
style={{
position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
background: 'var(--bg-primary, #fff)', border: '1px solid var(--border-color, #e0e0e0)',
borderRadius: 8, boxShadow: '0 8px 24px rgba(0,0,0,0.18)', padding: 16, minWidth: 220,
}}
onClick={(e) => e.stopPropagation()}
>
<h4 style={{ margin: '0 0 8px', fontSize: '0.9rem' }}>Scope ändern</h4>
<p style={{ margin: '0 0 12px', fontSize: '0.8rem', color: 'var(--text-secondary, #666)' }}>
Aktuell: <strong>{SCOPE_LABELS[currentScope]}</strong>
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{scopes.map(s => (
<button
key={s}
onClick={() => { handleShare(tpl, s); setScopeMenuId(null); }}
disabled={s === currentScope || sharingId === tpl.id}
style={{
padding: '6px 12px', border: '1px solid var(--border-color, #ddd)',
borderRadius: 4, background: s === currentScope ? 'var(--bg-secondary, #f0f0f0)' : 'transparent',
cursor: s === currentScope ? 'default' : 'pointer', textAlign: 'left', fontSize: '0.85rem',
fontWeight: s === currentScope ? 600 : 400,
}}
>
{SCOPE_LABELS[s]} {s === currentScope && '(aktuell)'}
</button>
))}
</div>
<button
onClick={() => setScopeMenuId(null)}
style={{ marginTop: 12, padding: '4px 12px', border: '1px solid var(--border-color, #ddd)', borderRadius: 4, background: 'transparent', cursor: 'pointer', fontSize: '0.8rem' }}
>
Abbrechen
</button>
</div>
</div>
);
})()}
<PromptDialog />
</div> </div>
); );
}; };

View file

@ -8,7 +8,8 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { FaPlay, FaSync, FaCheck, FaBan } from 'react-icons/fa'; import { FaPlay, FaSync, FaCheck, FaBan, FaPen } from 'react-icons/fa';
import { usePrompt } from '../../../hooks/usePrompt';
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator'; import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator';
import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useApiRequest } from '../../../hooks/useApi'; import { useApiRequest } from '../../../hooks/useApi';
@ -42,6 +43,7 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
const { request } = useApiRequest(); const { request } = useApiRequest();
const navigate = useNavigate(); const navigate = useNavigate();
const { showSuccess, showError } = useToast(); const { showSuccess, showError } = useToast();
const { prompt: promptInput, PromptDialog } = usePrompt();
const [workflows, setWorkflows] = useState<Automation2Workflow[]>([]); const [workflows, setWorkflows] = useState<Automation2Workflow[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -123,6 +125,26 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
[instanceId, request, showSuccess, showError, load] [instanceId, request, showSuccess, showError, load]
); );
const handleRename = useCallback(
async (row: Automation2Workflow) => {
if (!instanceId) return;
const newLabel = await promptInput('Neuer Name:', {
title: 'Workflow umbenennen',
defaultValue: row.label,
placeholder: 'Workflow-Name',
});
if (!newLabel || newLabel.trim() === row.label) return;
try {
await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() });
showSuccess('Workflow umbenannt');
await load();
} catch (e: any) {
showError(`Fehler: ${e?.message || 'Umbenennen fehlgeschlagen'}`);
}
},
[instanceId, request, promptInput, showSuccess, showError, load]
);
const handleExecute = useCallback( const handleExecute = useCallback(
async (row: Automation2Workflow) => { async (row: Automation2Workflow) => {
if (!instanceId) return; if (!instanceId) return;
@ -282,6 +304,12 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
}, },
]} ]}
customActions={[ customActions={[
{
id: 'rename',
icon: <FaPen />,
title: 'Umbenennen',
onClick: (row) => handleRename(row),
},
{ {
id: 'activate', id: 'activate',
icon: <FaCheck />, icon: <FaCheck />,
@ -312,6 +340,7 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
emptyMessage="Keine Workflows gefunden. Erstelle einen im Editor." emptyMessage="Keine Workflows gefunden. Erstelle einen im Editor."
/> />
</div> </div>
<PromptDialog />
</div> </div>
); );
}; };

View file

@ -11,39 +11,39 @@
--button-secondary-bg-disabled: var(--color-gray-disabled); --button-secondary-bg-disabled: var(--color-gray-disabled);
--button-secondary-text: var(--color-text); --button-secondary-text: var(--color-text);
--button-danger-bg: #dc3545; --button-danger-bg: var(--color-red, #C53030);
--button-danger-bg-hover: #c82333; --button-danger-bg-hover: #9B2C2C;
--button-danger-bg-disabled: #dc3545; --button-danger-bg-disabled: rgba(197, 48, 48, 0.4);
--button-danger-text: white; --button-danger-text: white;
--button-success-bg: #28a745; --button-success-bg: var(--color-success, #38A169);
--button-success-bg-hover: #218838; --button-success-bg-hover: #2F855A;
--button-success-bg-disabled: #28a745; --button-success-bg-disabled: rgba(56, 161, 105, 0.4);
--button-success-text: white; --button-success-text: white;
--button-warning-bg: #ffc107; --button-warning-bg: #D69E2E;
--button-warning-bg-hover: #e0a800; --button-warning-bg-hover: #B7791F;
--button-warning-bg-disabled: #ffc107; --button-warning-bg-disabled: rgba(214, 158, 46, 0.4);
--button-warning-text: #212529; --button-warning-text: white;
/* Button Sizes */ /* Button Sizes */
--button-sm-padding: 8px 12px; --button-sm-padding: 6px 14px;
--button-sm-font-size: 12px; --button-sm-font-size: 12px;
--button-sm-icon-size: 14px; --button-sm-icon-size: 14px;
--button-md-padding: 10px 20px; --button-md-padding: 8px 18px;
--button-md-font-size: 14px; --button-md-font-size: 14px;
--button-md-icon-size: 16px; --button-md-icon-size: 16px;
--button-lg-padding: 12px 24px; --button-lg-padding: 10px 22px;
--button-lg-font-size: 16px; --button-lg-font-size: 15px;
--button-lg-icon-size: 18px; --button-lg-icon-size: 18px;
/* Button Border Radius */ /* Button Border Radius */
--button-border-radius: 30px; --button-border-radius: 6px;
/* Button Transitions */ /* Button Transitions */
--button-transition: all 0.2s ease; --button-transition: all 0.15s ease;
} }
/* Base Button Styles */ /* Base Button Styles */
@ -66,17 +66,17 @@
} }
.button:focus { .button:focus {
box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb), 0.3); box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb, 74, 111, 165), 0.25);
} }
.button:disabled { .button:disabled {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.6; opacity: 0.5;
} }
.button.loading { .button.loading {
cursor: not-allowed; cursor: not-allowed;
opacity: 0.7; opacity: 0.65;
} }
.button.loading .buttonIcon { .button.loading .buttonIcon {
@ -104,7 +104,7 @@
} }
.buttonSecondary:hover:not(:disabled) { .buttonSecondary:hover:not(:disabled) {
background: var(--color-secondary-hover); background: var(--color-gray);
color: white; color: white;
} }
@ -211,17 +211,17 @@
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.buttonSm { .buttonSm {
padding: 4px 8px; padding: 4px 10px;
font-size: 11px; font-size: 11px;
} }
.buttonMd { .buttonMd {
padding: 8px 16px; padding: 6px 14px;
font-size: 13px; font-size: 13px;
} }
.buttonLg { .buttonLg {
padding: 10px 20px; padding: 8px 18px;
font-size: 15px; font-size: 14px;
} }
} }

View file

@ -52,10 +52,10 @@
.pageSubtitle { .pageSubtitle {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: var(--color-secondary); color: var(--color-text-secondary, #4A5568);
margin: 0.5rem 0 0 0; margin: 0.5rem 0 0 0;
font-family: var(--font-family); font-family: var(--font-family);
letter-spacing: 0.5px; letter-spacing: 0.3px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@ -230,7 +230,7 @@
/* Horizontal divider lines */ /* Horizontal divider lines */
.horizontalDivider { .horizontalDivider {
width: calc(100% + 60px); width: calc(100% + 60px);
background-color: var(--color-primary); background-color: var(--color-border, #E2E8F0);
height: 1px; height: 1px;
margin-left: -25px; margin-left: -25px;
margin-bottom: 0; margin-bottom: 0;
@ -465,7 +465,7 @@
} }
.chatHistoryNewChatButton { .chatHistoryNewChatButton {
border-radius: 25px !important; border-radius: var(--button-border-radius, 6px) !important;
} }
.chatHistoryEmpty { .chatHistoryEmpty {
@ -532,14 +532,14 @@
resize: none !important; resize: none !important;
flex: 1; flex: 1;
box-sizing: border-box; box-sizing: border-box;
border-radius: 25px !important; border-radius: 8px !important;
border: 1px solid var(--color-primary) !important; border: 1px solid var(--color-border, #E2E8F0) !important;
box-shadow: none !important; box-shadow: none !important;
} }
.textAreaFixed :global(textarea:focus) { .textAreaFixed :global(textarea:focus) {
border: 1px solid var(--color-secondary) !important; border: 1px solid var(--color-secondary) !important;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15) !important; box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb, 74, 111, 165), 0.15) !important;
outline: none !important; outline: none !important;
} }

View file

@ -1,47 +1,47 @@
:root { :root {
/* Original color definitions */
--color-bg: #F8F9FA; --color-bg: #F8F9FA;
--color-surface: #EFEDE5; --color-surface: #EEF0F2;
--color-text: #3A3A3A; --color-text: #2D3748;
--color-primary: #C7C5B2; --color-primary: #4A6FA5;
--color-primary-hover: #D9D7C6; --color-primary-hover: #3D5D8A;
--color-primary-disabled: #E3E2D8; --color-primary-disabled: rgba(74, 111, 165, 0.3);
--color-secondary: #F25843; --color-secondary: #4A6FA5;
--color-secondary-hover: #FF6A55; --color-secondary-hover: #3D5D8A;
--color-secondary-disabled: #F5B0A4; --color-secondary-disabled: rgba(74, 111, 165, 0.35);
--color-secondary-rgb: 74, 111, 165;
--color-red: #dc3545; --color-red: #C53030;
--color-red-hover: #f5c6cb; --color-red-hover: #E2B6B6;
--color-red-disabled: #f8d7da; --color-red-disabled: #F5D5D5;
--color-secondary-red: #B94A55; --color-secondary-red: #C53030;
--color-secondary-red-hover: #D46872; --color-secondary-red-hover: #9B2C2C;
--color-secondary-red-disabled: #E8B7BA; --color-secondary-red-disabled: #E2B6B6;
--color-gray: #6F7373; --color-gray: #718096;
--color-gray-hover: #565A5A; --color-gray-hover: #4A5568;
--color-gray-disabled: #B7BBBA; --color-gray-disabled: #CBD5E0;
--color-medium-gray: #E0DDD3; --color-medium-gray: #E2E8F0;
--color-medium-gray-hover: #D1CEC5; --color-medium-gray-hover: #CBD5E0;
--color-medium-gray-disabled: #E0DDD380; --color-medium-gray-disabled: rgba(226, 232, 240, 0.5);
--color-highlight-gray: #F5F3ED; --color-highlight-gray: #F7FAFC;
--color-highlight-gray-hover: #E6E3DC; --color-highlight-gray-hover: #EDF2F7;
--color-highlight-gray-disabled: #F5F3ED80; --color-highlight-gray-disabled: rgba(247, 250, 252, 0.5);
--color-success: #10b981; --color-success: #38A169;
--color-success-hover: #059669; --color-success-hover: #2F855A;
--color-success-disabled: #a7f3d0; --color-success-disabled: #C6F6D5;
--color-text-primary: #3A3A3A; /*to be deleted*/ --color-text-primary: #2D3748;
--font-family: "DM Sans", sans-serif; --font-family: "DM Sans", sans-serif;
--object-radius-large: 30px; --object-radius-large: 10px;
--object-radius-medium: 15px; --object-radius-medium: 8px;
--object-radius-small: 5px; --object-radius-small: 4px;
} }
/* ============================================== */ /* ============================================== */
@ -50,40 +50,40 @@
:root, .light-theme { :root, .light-theme {
/* Background colors */ /* Background colors */
--bg-primary: #ffffff; --bg-primary: #ffffff;
--bg-secondary: #F8F9FA; --bg-secondary: #F7FAFC;
--bg-dark: #f5f5f5; --bg-dark: #EDF2F7;
/* Surface colors */ /* Surface colors */
--surface-color: #f8f9fa; --surface-color: #F7FAFC;
--surface-dark: #f0f0f0; --surface-dark: #EDF2F7;
/* Text colors */ /* Text colors */
--text-primary: #1a1a1a; --text-primary: #1A202C;
--text-secondary: #666666; --text-secondary: #4A5568;
--text-tertiary: #888888; --text-tertiary: #718096;
--text-primary-dark: #1a1a1a; --text-primary-dark: #1A202C;
--text-secondary-dark: #666666; --text-secondary-dark: #4A5568;
--text-tertiary-dark: #888888; --text-tertiary-dark: #718096;
/* Border colors */ /* Border colors */
--border-color: #e0e0e0; --border-color: #E2E8F0;
--border-dark: #d0d0d0; --border-dark: #CBD5E0;
/* Primary accent color */ /* Primary accent color */
--primary-color: #F25843; --primary-color: #4A6FA5;
--primary-color-dark: #D94A37; --primary-color-dark: #3D5D8A;
--primary-color-light: rgba(242, 88, 67, 0.2); --primary-color-light: rgba(74, 111, 165, 0.15);
--primary-light: rgba(242, 88, 67, 0.12); --primary-light: rgba(74, 111, 165, 0.1);
--primary-dark-bg: rgba(242, 88, 67, 0.08); --primary-dark-bg: rgba(74, 111, 165, 0.06);
/* Hover backgrounds */ /* Hover backgrounds */
--hover-bg: rgba(0, 0, 0, 0.04); --hover-bg: rgba(0, 0, 0, 0.03);
--hover-bg-dark: rgba(0, 0, 0, 0.06); --hover-bg-dark: rgba(0, 0, 0, 0.05);
/* Error color */ /* Error color */
--error-color: #dc2626; --error-color: #C53030;
/* Legacy / inline-style aliases (override :root beige --color-primary) */ /* Legacy aliases */
--color-primary: var(--primary-color); --color-primary: var(--primary-color);
--color-primary-hover: var(--primary-color-dark); --color-primary-hover: var(--primary-color-dark);
--color-primary-disabled: var(--primary-color-light); --color-primary-disabled: var(--primary-color-light);
@ -97,68 +97,68 @@
/* DARK THEME */ /* DARK THEME */
/* ============================================== */ /* ============================================== */
.dark-theme { .dark-theme {
--color-bg: #181818; --color-bg: #1A202C;
--color-surface: #1E1D1A; --color-surface: #2D3748;
--color-text: #E5E7EB; --color-text: #E2E8F0;
--color-primary: var(--primary-color); --color-primary: var(--primary-color);
--color-primary-hover: var(--primary-color-dark); --color-primary-hover: var(--primary-color-dark);
--color-primary-disabled: rgba(242, 88, 67, 0.35); --color-primary-disabled: rgba(74, 111, 165, 0.35);
--color-secondary: #F25843; --color-secondary: #5A8AC5;
--color-secondary-hover: #FF715C; --color-secondary-hover: #4A7AB5;
--color-secondary-disabled: #6E3E36; --color-secondary-disabled: #2C4A6E;
--color-secondary-rgb: 90, 138, 197;
--color-red: #dc3545; --color-red: #FC8181;
--color-red-hover: #f5c6cb; --color-red-hover: #FEB2B2;
--color-red-disabled: #f8d7da; --color-red-disabled: #742A2A;
--color-secondary-red: #D65D6A; --color-secondary-red: #FC8181;
--color-secondary-red-hover: #E17683; --color-secondary-red-hover: #FEB2B2;
--color-secondary-red-disabled: #70363C; --color-secondary-red-disabled: #742A2A;
/* Readable neutrals on dark (was #181818 — same as bg, illegible) */ --color-gray: #A0AEC0;
--color-gray: #9ca3af; --color-gray-hover: #CBD5E0;
--color-gray-hover: #d1d5db; --color-gray-disabled: #4A5568;
--color-gray-disabled: #57534e;
/* Background colors */ /* Background colors */
--bg-primary: #181818; --bg-primary: #1A202C;
--bg-secondary: #1E1D1A; --bg-secondary: #2D3748;
--bg-dark: #0a0a0a; --bg-dark: #171923;
/* Surface colors */ /* Surface colors */
--surface-color: #1E1D1A; --surface-color: #2D3748;
--surface-dark: #1a1a1a; --surface-dark: #1A202C;
/* Text colors */ /* Text colors */
--text-primary: #E5E7EB; --text-primary: #E2E8F0;
--text-secondary: #C7C5B2; --text-secondary: #A0AEC0;
--text-tertiary: #9CA3AF; --text-tertiary: #718096;
--text-primary-dark: #E5E7EB; --text-primary-dark: #E2E8F0;
--text-secondary-dark: #C7C5B2; --text-secondary-dark: #A0AEC0;
--text-tertiary-dark: #9CA3AF; --text-tertiary-dark: #718096;
/* Border colors */ /* Border colors */
--border-color: rgba(199, 197, 178, 0.15); --border-color: rgba(226, 232, 240, 0.12);
--border-dark: rgba(199, 197, 178, 0.15); --border-dark: rgba(226, 232, 240, 0.12);
/* Primary accent color */ /* Primary accent color */
--primary-color: #F25843; --primary-color: #5A8AC5;
--primary-color-dark: #D94A37; --primary-color-dark: #4A7AB5;
--primary-color-light: rgba(242, 88, 67, 0.3); --primary-color-light: rgba(90, 138, 197, 0.3);
--primary-light: #FF9A8A; /* Lighter red for text on dark backgrounds */ --primary-light: #7BA7D7;
--primary-dark-bg: rgba(242, 88, 67, 0.15); /* Semi-transparent red for backgrounds */ --primary-dark-bg: rgba(90, 138, 197, 0.12);
/* Hover backgrounds */ /* Hover backgrounds */
--hover-bg: rgba(255, 255, 255, 0.06); --hover-bg: rgba(255, 255, 255, 0.05);
--hover-bg-dark: rgba(255, 255, 255, 0.06); --hover-bg-dark: rgba(255, 255, 255, 0.05);
/* Error color */ /* Error color */
--error-color: #ef4444; --error-color: #FC8181;
--color-border: var(--border-color); --color-border: var(--border-color);
--bg-card: #252422; --bg-card: #2D3748;
--bg-input: #2c2926; --bg-input: #2D3748;
--bg-hover: rgba(255, 255, 255, 0.08); --bg-hover: rgba(255, 255, 255, 0.06);
} }

View file

@ -259,7 +259,9 @@ export const FEATURE_REGISTRY: Record<string, FeatureConfig> = {
views: [ views: [
{ code: 'editor', label: { de: 'Editor', en: 'Editor' }, path: 'editor' }, { code: 'editor', label: { de: 'Editor', en: 'Editor' }, path: 'editor' },
{ code: 'workflows', label: { de: 'Workflows', en: 'Workflows' }, path: 'workflows' }, { code: 'workflows', label: { de: 'Workflows', en: 'Workflows' }, path: 'workflows' },
{ code: 'templates', label: { de: 'Vorlagen', en: 'Templates' }, path: 'templates' },
{ code: 'workflows-tasks', label: { de: 'Tasks', en: 'Tasks' }, path: 'workflows-tasks' }, { code: 'workflows-tasks', label: { de: 'Tasks', en: 'Tasks' }, path: 'workflows-tasks' },
{ code: 'dashboard', label: { de: 'Dashboard', en: 'Dashboard' }, path: 'dashboard' },
] ]
}, },
neutralization: { neutralization: {