integrated and initial teste unified automation
This commit is contained in:
parent
3a6f90a497
commit
bc977b8281
28 changed files with 1287 additions and 734 deletions
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue