corrad-bp/components/ProcessTemplatesModal.vue
Md Afiq Iskandar c5edf2abfe Enhance Process Templates Modal with Preview Functionality and Styling Updates
- Introduced a preview modal for process templates, allowing users to view detailed information and flow before selection.
- Updated the template card styles for improved visual appeal and user interaction, including hover effects and background gradients.
- Enhanced the layout and structure of the template display, ensuring better organization of template details and action buttons.
- Added computed properties for dynamic node styling based on template definitions, improving the visual representation of nodes in the preview.
- Implemented responsive design adjustments to ensure usability across different screen sizes.
2025-07-21 13:30:41 +08:00

1730 lines
54 KiB
Vue

<template>
<RsModal v-model="showModal" title="Process Flow Templates" size="xl" position="center">
<div class="process-templates-container">
<!-- Templates Categories -->
<div class="mb-6">
<div class="flex space-x-2 mb-4 border-b border-gray-200">
<button
v-for="cat in categories"
:key="cat.id"
@click="activeCategory = cat.id"
class="px-4 py-2 text-sm font-medium transition-colors duration-200"
:class="activeCategory === cat.id ? 'border-b-2 border-blue-500 text-blue-600 bg-blue-50' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'"
>
{{ cat.name }}
</button>
</div>
</div>
<!-- Templates Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div v-for="template in filteredTemplates" :key="template.id" class="template-card border border-gray-200 rounded-lg overflow-hidden shadow-sm hover:shadow-lg transition-all duration-300 bg-white">
<div class="template-preview bg-gradient-to-br from-gray-50 to-gray-100 p-4 border-b border-gray-200 relative">
<div class="w-full h-full flex flex-col items-center justify-center">
<div class="template-icon-wrapper">
<Icon :name="getTemplateIcon(template)" class="w-12 h-12 text-gray-500" />
</div>
<span class="text-sm text-gray-600 mt-2 font-medium">{{ template.name }}</span>
</div>
<!-- Preview overlay -->
<div class="absolute inset-0 bg-black bg-opacity-0 hover:bg-opacity-10 transition-all duration-200 flex items-center justify-center">
<button
@click="previewTemplate(template)"
class="preview-btn opacity-0 hover:opacity-100 transition-opacity duration-200 bg-white bg-opacity-90 hover:bg-opacity-100 text-gray-700 px-3 py-1 rounded-full text-xs font-medium shadow-md border border-gray-200"
>
<Icon name="material-symbols:visibility" class="w-4 h-4 mr-1" />
Preview
</button>
</div>
</div>
<div class="p-4">
<h3 class="font-semibold text-lg mb-2 text-gray-800">{{ template.name }}</h3>
<p class="text-gray-600 text-sm mb-4 leading-relaxed">{{ template.description }}</p>
<div class="flex items-center text-xs text-gray-500 mb-4 space-x-4">
<div class="flex items-center">
<Icon name="material-symbols:account-tree" class="mr-1 w-3 h-3" />
<span>{{ template.nodeCount }} nodes</span>
</div>
<div class="flex items-center">
<Icon name="material-symbols:route" class="mr-1 w-3 h-3" />
<span>{{ template.edgeCount }} connections</span>
</div>
<div class="flex items-center">
<Icon name="material-symbols:timer" class="mr-1 w-3 h-3" />
<span class="capitalize">{{ template.complexity }}</span>
</div>
</div>
<div class="flex space-x-2">
<RsButton
@click="previewTemplate(template)"
variant="secondary"
size="sm"
class="flex-1"
>
<Icon name="material-symbols:visibility" class="w-4 h-4 mr-1" />
Preview
</RsButton>
<RsButton
@click="useTemplate(template)"
variant="primary"
size="sm"
class="flex-1"
>
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
Use Template
</RsButton>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-if="filteredTemplates.length === 0" class="flex flex-col items-center justify-center py-12 text-gray-500">
<Icon name="material-symbols:category-outline" class="w-16 h-16 mb-4 text-gray-300" />
<p class="text-base font-medium">No templates found</p>
<p class="text-sm mt-1">Try selecting a different category</p>
</div>
</div>
</RsModal>
<!-- Preview Modal -->
<RsModal v-model="showPreviewModal" title="Template Preview" size="2xl" position="center">
<div v-if="selectedTemplate" class="template-preview-container">
<!-- Template Info -->
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
<div class="flex items-start justify-between mb-3">
<div>
<h2 class="text-xl font-semibold text-gray-800 mb-1">{{ selectedTemplate.name }}</h2>
<p class="text-gray-600">{{ selectedTemplate.description }}</p>
</div>
<div class="flex items-center space-x-2">
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full capitalize">
{{ selectedTemplate.complexity }}
</span>
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full">
{{ selectedTemplate.category }}
</span>
</div>
</div>
<div class="flex items-center space-x-6 text-sm text-gray-600">
<div class="flex items-center">
<Icon name="material-symbols:account-tree" class="mr-1 w-4 h-4" />
<span>{{ selectedTemplate.nodeCount }} nodes</span>
</div>
<div class="flex items-center">
<Icon name="material-symbols:route" class="mr-1 w-4 h-4" />
<span>{{ selectedTemplate.edgeCount }} connections</span>
</div>
<div class="flex items-center">
<Icon name="material-symbols:data-object" class="mr-1 w-4 h-4" />
<span>{{ selectedTemplate.variables?.length || 0 }} variables</span>
</div>
</div>
</div>
<!-- Process Flow Preview -->
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-800 mb-3">Process Flow</h3>
<div class="process-flow-preview bg-white border border-gray-200 rounded-lg h-96 overflow-hidden">
<VueFlow
v-model="previewFlow"
:nodes="previewNodes"
:edges="previewEdges"
:node-types="customNodeTypes"
:default-viewport="{ x: 0, y: 0, zoom: 0.8 }"
:min-zoom="0.5"
:max-zoom="1.5"
:pan-on-drag="true"
:zoom-on-scroll="true"
:pan-on-scroll="false"
:zoom-on-double-click="false"
:select-nodes-on-drag="false"
:nodes-draggable="false"
:nodes-connectable="false"
:elements-selectable="false"
:delete-key-code="null"
class="preview-vue-flow"
>
<Background pattern-color="#e2e8f0" gap="20" />
<Controls position="bottom-right" />
<MiniMap position="top-right" />
</VueFlow>
</div>
</div>
<!-- Variables -->
<div class="mb-6" v-if="selectedTemplate.variables && selectedTemplate.variables.length > 0">
<h3 class="text-lg font-medium text-gray-800 mb-3">Process Variables</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div
v-for="variable in selectedTemplate.variables"
:key="variable.name"
class="variable-item p-3 bg-gray-50 rounded-lg border border-gray-200"
>
<div class="flex items-center justify-between mb-1">
<span class="font-medium text-sm text-gray-800">{{ variable.name }}</span>
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
{{ variable.type }}
</span>
</div>
<p class="text-xs text-gray-600">{{ variable.description }}</p>
<div v-if="variable.defaultValue !== undefined && variable.defaultValue !== ''" class="mt-1">
<span class="text-xs text-gray-500">Default: </span>
<span class="text-xs font-mono bg-gray-200 px-1 rounded">{{ variable.defaultValue }}</span>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-200">
<RsButton @click="showPreviewModal = false" variant="secondary" size="sm">
Close
</RsButton>
<RsButton @click="useTemplateFromPreview" variant="primary" size="sm">
<Icon name="material-symbols:add" class="w-4 h-4 mr-1" />
Use This Template
</RsButton>
</div>
</div>
</RsModal>
</template>
<script setup>
import { VueFlow } from '@vue-flow/core';
import { Background } from '@vue-flow/background';
import { Controls } from '@vue-flow/controls';
import { MiniMap } from '@vue-flow/minimap';
import { markRaw } from 'vue';
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import "@vue-flow/controls/dist/style.css";
import "@vue-flow/minimap/dist/style.css";
// Import custom node components
import StartNode from "~/components/process-flow/custom/StartNode.vue";
import EndNode from "~/components/process-flow/custom/EndNode.vue";
import FormNode from "~/components/process-flow/custom/FormNode.vue";
import ApiNode from "~/components/process-flow/custom/ApiNode.vue";
import GatewayNode from "~/components/process-flow/custom/GatewayNode.vue";
import ScriptNode from "~/components/process-flow/custom/ScriptNode.vue";
import BusinessRuleNode from "~/components/process-flow/custom/BusinessRuleNode.vue";
import NotificationNode from "~/components/process-flow/custom/NotificationNode.vue";
import HtmlNode from "~/components/process-flow/custom/HtmlNode.vue";
import SubprocessNode from "~/components/process-flow/custom/SubprocessNode.vue";
// Create nodeTypes object with markRaw to prevent reactivity issues
const customNodeTypes = {
'start': markRaw(StartNode),
'end': markRaw(EndNode),
'form': markRaw(FormNode),
'api': markRaw(ApiNode),
'gateway': markRaw(GatewayNode),
'script': markRaw(ScriptNode),
'business-rule': markRaw(BusinessRuleNode),
'notification': markRaw(NotificationNode),
'html': markRaw(HtmlNode),
'subprocess': markRaw(SubprocessNode)
};
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue', 'select-template']);
const showModal = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
// Preview modal state
const showPreviewModal = ref(false);
const selectedTemplate = ref(null);
// Categories
const categories = [
{ id: 'all', name: 'All Templates' },
{ id: 'approval', name: 'Approval Workflows' },
{ id: 'onboarding', name: 'Onboarding' },
{ id: 'support', name: 'Support & Service' },
{ id: 'business', name: 'Business Operations' },
{ id: 'automation', name: 'Automation' }
];
// Active category
const activeCategory = ref('all');
// Process flow templates
const templates = [
{
id: 'simple-approval',
name: 'Simple Approval Workflow',
description: 'A basic approval process with form submission, review, and notification.',
category: 'approval',
complexity: 'Simple',
nodeCount: 6,
edgeCount: 5,
variables: [
{
name: 'approval_status',
type: 'string',
scope: 'process',
description: 'Status of the approval request (approved/rejected)',
defaultValue: 'pending'
},
{
name: 'requester_email',
type: 'string',
scope: 'process',
description: 'Email address of the person who submitted the request',
defaultValue: ''
},
{
name: 'manager_email',
type: 'string',
scope: 'process',
description: 'Email address of the approving manager',
defaultValue: ''
}
],
nodes: [
{
id: 'start-1',
type: 'start',
position: { x: 100, y: 100 },
data: {
label: 'Start',
description: 'Begin approval process',
backgroundColor: '#dcfce7',
borderColor: '#10b981',
textColor: '#065f46',
shape: 'circle'
}
},
{
id: 'form-1',
type: 'form',
position: { x: 300, y: 100 },
data: {
label: 'Submit Request',
description: 'Employee submits approval request',
formId: null,
formName: 'Approval Request Form',
backgroundColor: '#faf5ff',
borderColor: '#9333ea',
textColor: '#6b21a8',
shape: 'rectangle'
}
},
{
id: 'notification-1',
type: 'notification',
position: { x: 500, y: 100 },
data: {
label: 'Notify Manager',
description: 'Send notification to manager',
notificationType: 'info',
recipientType: 'role',
recipientRole: 'manager',
subject: 'New approval request',
message: 'A new approval request requires your review.',
priority: 'medium',
backgroundColor: '#f0f9ff',
borderColor: '#0ea5e9',
textColor: '#0284c7',
shape: 'rectangle'
}
},
{
id: 'form-2',
type: 'form',
position: { x: 700, y: 100 },
data: {
label: 'Manager Review',
description: 'Manager reviews and approves/rejects',
formId: null,
formName: 'Manager Review Form',
backgroundColor: '#faf5ff',
borderColor: '#9333ea',
textColor: '#6b21a8',
shape: 'rectangle'
}
},
{
id: 'gateway-1',
type: 'gateway',
position: { x: 900, y: 100 },
data: {
label: 'Approved?',
description: 'Check if request was approved',
conditions: [
{
variable: 'approval_status',
operator: 'eq',
value: 'approved',
output: 'Approved'
}
],
defaultPath: 'Rejected',
backgroundColor: '#fff7ed',
borderColor: '#f97316',
textColor: '#c2410c',
shape: 'diamond'
}
},
{
id: 'notification-2',
type: 'notification',
position: { x: 1100, y: 50 },
data: {
label: 'Notify Approval',
description: 'Send approval notification',
notificationType: 'success',
recipientType: 'variable',
recipientVariable: 'requester_email',
subject: 'Request Approved',
message: 'Your request has been approved.',
priority: 'high',
backgroundColor: '#f0f9ff',
borderColor: '#0ea5e9',
textColor: '#0284c7',
shape: 'rectangle'
}
},
{
id: 'notification-3',
type: 'notification',
position: { x: 1100, y: 150 },
data: {
label: 'Notify Rejection',
description: 'Send rejection notification',
notificationType: 'warning',
recipientType: 'variable',
recipientVariable: 'requester_email',
subject: 'Request Rejected',
message: 'Your request has been rejected.',
priority: 'medium',
backgroundColor: '#f0f9ff',
borderColor: '#0ea5e9',
textColor: '#0284c7',
shape: 'rectangle'
}
},
{
id: 'end-1',
type: 'end',
position: { x: 1300, y: 100 },
data: {
label: 'End',
description: 'Process completed',
backgroundColor: '#fee2e2',
borderColor: '#dc2626',
textColor: '#991b1b',
shape: 'circle'
}
}
],
edges: [
{ id: 'e1', source: 'start-1', target: 'form-1', sourceHandle: 'start-1-bottom', targetHandle: 'form-1-top', type: 'smoothstep' },
{ id: 'e2', source: 'form-1', target: 'notification-1', sourceHandle: 'form-1-bottom', targetHandle: 'notification-1-top', type: 'smoothstep' },
{ id: 'e3', source: 'notification-1', target: 'form-2', sourceHandle: 'notification-1-bottom', targetHandle: 'form-2-top', type: 'smoothstep' },
{ id: 'e4', source: 'form-2', target: 'gateway-1', sourceHandle: 'form-2-bottom', targetHandle: 'gateway-1-top', type: 'smoothstep' },
{ id: 'e5', source: 'gateway-1', target: 'notification-2', sourceHandle: 'gateway-1-right', targetHandle: 'notification-2-left', type: 'smoothstep', label: 'Approved' },
{ id: 'e6', source: 'gateway-1', target: 'notification-3', sourceHandle: 'gateway-1-bottom', targetHandle: 'notification-3-left', type: 'smoothstep', label: 'Rejected' },
{ id: 'e7', source: 'notification-2', target: 'end-1', sourceHandle: 'notification-2-bottom', targetHandle: 'end-1-top', type: 'smoothstep' },
{ id: 'e8', source: 'notification-3', target: 'end-1', sourceHandle: 'notification-3-bottom', targetHandle: 'end-1-left', type: 'smoothstep' }
]
},
{
id: 'employee-onboarding',
name: 'Employee Onboarding',
description: 'Complete employee onboarding process with multiple stages and notifications.',
category: 'onboarding',
complexity: 'Complex',
nodeCount: 9,
edgeCount: 8,
variables: [
{
name: 'employee_name',
type: 'string',
scope: 'process',
description: 'Full name of the new employee',
defaultValue: ''
},
{
name: 'employee_email',
type: 'string',
scope: 'process',
description: 'Email address of the new employee',
defaultValue: ''
},
{
name: 'age',
type: 'int',
scope: 'process',
description: 'Age of the employee',
defaultValue: 18
},
{
name: 'age_valid',
type: 'boolean',
scope: 'process',
description: 'Whether the employee meets age requirements',
defaultValue: false
},
{
name: 'employee_account',
type: 'object',
scope: 'process',
description: 'Employee account creation response data',
defaultValue: {}
}
],
nodes: [
{
id: 'start-2',
type: 'start',
position: { x: 100, y: 200 },
data: {
label: 'Start',
description: 'Begin onboarding process'
}
},
{
id: 'form-3',
type: 'form',
position: { x: 300, y: 200 },
data: {
label: 'Personal Info',
description: 'Collect employee personal information',
formId: null,
formName: 'Employee Personal Information'
}
},
{
id: 'business-rule-1',
type: 'business-rule',
position: { x: 500, y: 200 },
data: {
label: 'Validate Info',
description: 'Validate employee information',
ruleGroups: [
{
name: 'Age Validation',
conditions: [
{
variable: 'age',
operator: 'gte',
value: '18',
minValue: null,
maxValue: null
}
],
actions: [
{
type: 'set_variable',
variable: 'age_valid',
value: 'true'
}
]
}
]
}
},
{
id: 'api-1',
type: 'api',
position: { x: 700, y: 200 },
data: {
label: 'Create Account',
description: 'Create employee account in system',
apiMethod: 'POST',
apiUrl: '/api/employees/create',
requestBody: '{"name": "{{employee_name}}", "email": "{{employee_email}}"}',
outputVariable: 'employee_account'
}
},
{
id: 'form-4',
type: 'form',
position: { x: 900, y: 200 },
data: {
label: 'Equipment Request',
description: 'Request necessary equipment',
formId: null,
formName: 'Equipment Request Form'
}
},
{
id: 'notification-4',
type: 'notification',
position: { x: 1100, y: 200 },
data: {
label: 'Notify IT',
description: 'Notify IT department about equipment',
notificationType: 'info',
recipientType: 'role',
recipientRole: 'it_admin',
subject: 'New employee equipment request',
message: 'Please prepare equipment for new employee.',
priority: 'medium'
}
},
{
id: 'form-5',
type: 'form',
position: { x: 1300, y: 200 },
data: {
label: 'Training Schedule',
description: 'Schedule training sessions',
formId: null,
formName: 'Training Schedule Form'
}
},
{
id: 'notification-5',
type: 'notification',
position: { x: 1500, y: 200 },
data: {
label: 'Welcome Email',
description: 'Send welcome email to employee',
notificationType: 'success',
recipientType: 'variable',
recipientVariable: 'employee_email',
subject: 'Welcome to the team!',
message: 'Welcome to our company! Your onboarding is complete.',
priority: 'high'
}
},
{
id: 'end-2',
type: 'end',
position: { x: 1700, y: 200 },
data: {
label: 'End',
description: 'Onboarding completed'
}
}
],
edges: [
{ id: 'e9', source: 'start-2', target: 'form-3', sourceHandle: 'start-2-bottom', targetHandle: 'form-3-top', type: 'smoothstep' },
{ id: 'e10', source: 'form-3', target: 'business-rule-1', sourceHandle: 'form-3-bottom', targetHandle: 'business-rule-1-top', type: 'smoothstep' },
{ id: 'e11', source: 'business-rule-1', target: 'api-1', sourceHandle: 'business-rule-1-bottom', targetHandle: 'api-1-top', type: 'smoothstep' },
{ id: 'e12', source: 'api-1', target: 'form-4', sourceHandle: 'api-1-bottom', targetHandle: 'form-4-top', type: 'smoothstep' },
{ id: 'e13', source: 'form-4', target: 'notification-4', sourceHandle: 'form-4-bottom', targetHandle: 'notification-4-top', type: 'smoothstep' },
{ id: 'e14', source: 'notification-4', target: 'form-5', sourceHandle: 'notification-4-bottom', targetHandle: 'form-5-top', type: 'smoothstep' },
{ id: 'e15', source: 'form-5', target: 'notification-5', sourceHandle: 'form-5-bottom', targetHandle: 'notification-5-top', type: 'smoothstep' },
{ id: 'e16', source: 'notification-5', target: 'end-2', sourceHandle: 'notification-5-bottom', targetHandle: 'end-2-top', type: 'smoothstep' }
]
},
{
id: 'support-ticket',
name: 'Support Ticket Process',
description: 'Customer support ticket workflow with escalation and resolution tracking.',
category: 'support',
complexity: 'Medium',
nodeCount: 8,
edgeCount: 9,
variables: [
{
name: 'issue_type',
type: 'string',
scope: 'process',
description: 'Type of issue reported (system_down, bug, feature_request, etc.)',
defaultValue: 'general'
},
{
name: 'priority',
type: 'string',
scope: 'process',
description: 'Priority level of the ticket (high, medium, low)',
defaultValue: 'medium'
},
{
name: 'customer_email',
type: 'string',
scope: 'process',
description: 'Email address of the customer who submitted the ticket',
defaultValue: ''
},
{
name: 'ticket_id',
type: 'string',
scope: 'process',
description: 'Unique identifier for the support ticket',
defaultValue: ''
}
],
nodes: [
{
id: 'start-3',
type: 'start',
position: { x: 100, y: 300 },
data: {
label: 'Start',
description: 'Customer support request initiated'
}
},
{
id: 'form-6',
type: 'form',
position: { x: 300, y: 300 },
data: {
label: 'Submit Ticket',
description: 'Customer submits support ticket',
formId: null,
formName: 'Support Ticket Form'
}
},
{
id: 'business-rule-2',
type: 'business-rule',
position: { x: 500, y: 300 },
data: {
label: 'Categorize Priority',
description: 'Automatically set ticket priority',
ruleGroups: [
{
name: 'High Priority',
conditions: [
{
variable: 'issue_type',
operator: 'eq',
value: 'system_down',
minValue: null,
maxValue: null
}
],
actions: [
{
type: 'set_variable',
variable: 'priority',
value: 'high'
}
]
},
{
name: 'Medium Priority',
conditions: [
{
variable: 'issue_type',
operator: 'eq',
value: 'bug',
minValue: null,
maxValue: null
}
],
actions: [
{
type: 'set_variable',
variable: 'priority',
value: 'medium'
}
]
}
]
}
},
{
id: 'gateway-2',
type: 'gateway',
position: { x: 700, y: 300 },
data: {
label: 'High Priority?',
description: 'Check if ticket is high priority',
conditions: [
{
variable: 'priority',
operator: 'eq',
value: 'high',
output: 'High Priority'
}
],
defaultPath: 'Normal Priority'
}
},
{
id: 'notification-6',
type: 'notification',
position: { x: 900, y: 200 },
data: {
label: 'Escalate to Senior',
description: 'Notify senior support team',
notificationType: 'warning',
recipientType: 'role',
recipientRole: 'senior_support',
subject: 'High priority ticket escalation',
message: 'High priority support ticket requires immediate attention.',
priority: 'high'
}
},
{
id: 'notification-7',
type: 'notification',
position: { x: 900, y: 400 },
data: {
label: 'Assign to Support',
description: 'Assign to regular support team',
notificationType: 'info',
recipientType: 'role',
recipientRole: 'support',
subject: 'New support ticket assigned',
message: 'New support ticket has been assigned to you.',
priority: 'medium'
}
},
{
id: 'form-7',
type: 'form',
position: { x: 1100, y: 300 },
data: {
label: 'Resolve Ticket',
description: 'Support agent resolves the ticket',
formId: null,
formName: 'Ticket Resolution Form'
}
},
{
id: 'notification-8',
type: 'notification',
position: { x: 1300, y: 300 },
data: {
label: 'Notify Customer',
description: 'Send resolution notification to customer',
notificationType: 'success',
recipientType: 'variable',
recipientVariable: 'customer_email',
subject: 'Your support ticket has been resolved',
message: 'Your support ticket has been resolved. Thank you for contacting us.',
priority: 'medium'
}
},
{
id: 'end-3',
type: 'end',
position: { x: 1500, y: 300 },
data: {
label: 'End',
description: 'Support ticket resolved'
}
}
],
edges: [
{ id: 'e17', source: 'start-3', target: 'form-6', sourceHandle: 'start-3-bottom', targetHandle: 'form-6-top', type: 'smoothstep' },
{ id: 'e18', source: 'form-6', target: 'business-rule-2', sourceHandle: 'form-6-bottom', targetHandle: 'business-rule-2-top', type: 'smoothstep' },
{ id: 'e19', source: 'business-rule-2', target: 'gateway-2', sourceHandle: 'business-rule-2-bottom', targetHandle: 'gateway-2-top', type: 'smoothstep' },
{ id: 'e20', source: 'gateway-2', target: 'notification-6', sourceHandle: 'gateway-2-right', targetHandle: 'notification-6-left', type: 'smoothstep', label: 'High Priority' },
{ id: 'e21', source: 'gateway-2', target: 'notification-7', sourceHandle: 'gateway-2-bottom', targetHandle: 'notification-7-left', type: 'smoothstep', label: 'Normal Priority' },
{ id: 'e22', source: 'notification-6', target: 'form-7', sourceHandle: 'notification-6-bottom', targetHandle: 'form-7-left', type: 'smoothstep' },
{ id: 'e23', source: 'notification-7', target: 'form-7', sourceHandle: 'notification-7-bottom', targetHandle: 'form-7-top', type: 'smoothstep' },
{ id: 'e24', source: 'form-7', target: 'notification-8', sourceHandle: 'form-7-bottom', targetHandle: 'notification-8-top', type: 'smoothstep' },
{ id: 'e25', source: 'notification-8', target: 'end-3', sourceHandle: 'notification-8-bottom', targetHandle: 'end-3-top', type: 'smoothstep' }
]
},
{
id: 'expense-reimbursement',
name: 'Expense Reimbursement',
description: 'Employee expense reimbursement with approval workflow and payment processing.',
category: 'business',
complexity: 'Medium',
nodeCount: 7,
edgeCount: 8,
variables: [
{
name: 'total_amount',
type: 'decimal',
scope: 'process',
description: 'Total amount of expenses claimed',
defaultValue: 0
},
{
name: 'auto_approve',
type: 'boolean',
scope: 'process',
description: 'Whether expenses can be auto-approved',
defaultValue: false
},
{
name: 'employee_id',
type: 'string',
scope: 'process',
description: 'ID of the employee submitting expenses',
defaultValue: ''
},
{
name: 'employee_email',
type: 'string',
scope: 'process',
description: 'Email of the employee submitting expenses',
defaultValue: ''
},
{
name: 'payment_result',
type: 'object',
scope: 'process',
description: 'Result of payment processing',
defaultValue: {}
}
],
nodes: [
{
id: 'start-4',
type: 'start',
position: { x: 100, y: 400 },
data: {
label: 'Start',
description: 'Begin expense reimbursement process'
}
},
{
id: 'form-8',
type: 'form',
position: { x: 300, y: 400 },
data: {
label: 'Submit Expenses',
description: 'Employee submits expense report',
formId: null,
formName: 'Expense Report Form'
}
},
{
id: 'business-rule-3',
type: 'business-rule',
position: { x: 500, y: 400 },
data: {
label: 'Validate Expenses',
description: 'Validate expense amounts and receipts',
ruleGroups: [
{
name: 'Amount Check',
conditions: [
{
variable: 'total_amount',
operator: 'lte',
value: '1000',
minValue: null,
maxValue: null
}
],
actions: [
{ type: 'set_variable', variable: 'auto_approve', value: 'true' }
]
}
]
}
},
{
id: 'gateway-3',
type: 'gateway',
position: { x: 700, y: 400 },
data: {
label: 'Auto Approve?',
description: 'Check if expenses can be auto-approved',
conditions: [
{
variable: 'auto_approve',
operator: 'eq',
value: 'true',
output: 'Auto Approve'
}
],
defaultPath: 'Needs Approval'
}
},
{
id: 'form-9',
type: 'form',
position: { x: 900, y: 300 },
data: {
label: 'Manager Approval',
description: 'Manager reviews and approves expenses',
formId: null,
formName: 'Manager Approval Form'
}
},
{
id: 'api-2',
type: 'api',
position: { x: 1100, y: 400 },
data: {
label: 'Process Payment',
description: 'Process reimbursement payment',
apiMethod: 'POST',
apiUrl: '/api/payments/reimburse',
requestBody: '{"employee_id": "{{employee_id}}", "amount": "{{total_amount}}"}',
outputVariable: 'payment_result'
}
},
{
id: 'notification-9',
type: 'notification',
position: { x: 1300, y: 400 },
data: {
label: 'Payment Confirmation',
description: 'Notify employee of payment',
notificationType: 'success',
recipientType: 'variable',
recipientVariable: 'employee_email',
subject: 'Expense reimbursement processed',
message: 'Your expense reimbursement has been processed and payment is on the way.',
priority: 'medium'
}
},
{
id: 'end-4',
type: 'end',
position: { x: 1500, y: 400 },
data: {
label: 'End',
description: 'Reimbursement completed'
}
}
],
edges: [
{ id: 'e26', source: 'start-4', target: 'form-8', sourceHandle: 'start-4-bottom', targetHandle: 'form-8-top', type: 'smoothstep' },
{ id: 'e27', source: 'form-8', target: 'business-rule-3', sourceHandle: 'form-8-bottom', targetHandle: 'business-rule-3-top', type: 'smoothstep' },
{ id: 'e28', source: 'business-rule-3', target: 'gateway-3', sourceHandle: 'business-rule-3-bottom', targetHandle: 'gateway-3-top', type: 'smoothstep' },
{ id: 'e29', source: 'gateway-3', target: 'form-9', sourceHandle: 'gateway-3-right', targetHandle: 'form-9-left', type: 'smoothstep', label: 'Needs Approval' },
{ id: 'e30', source: 'gateway-3', target: 'api-2', sourceHandle: 'gateway-3-bottom', targetHandle: 'api-2-left', type: 'smoothstep', label: 'Auto Approve' },
{ id: 'e31', source: 'form-9', target: 'api-2', sourceHandle: 'form-9-bottom', targetHandle: 'api-2-top', type: 'smoothstep' },
{ id: 'e32', source: 'api-2', target: 'notification-9', sourceHandle: 'api-2-bottom', targetHandle: 'notification-9-top', type: 'smoothstep' },
{ id: 'e33', source: 'notification-9', target: 'end-4', sourceHandle: 'notification-9-bottom', targetHandle: 'end-4-top', type: 'smoothstep' }
]
},
{
id: 'automated-backup',
name: 'Automated Data Backup',
description: 'Automated data backup process with validation and notification.',
category: 'automation',
complexity: 'Simple',
nodeCount: 6,
edgeCount: 6,
variables: [
{
name: 'current_timestamp',
type: 'datetime',
scope: 'process',
description: 'Current timestamp for backup operation',
defaultValue: ''
},
{
name: 'backup_result',
type: 'object',
scope: 'process',
description: 'Result of backup operation',
defaultValue: {}
},
{
name: 'backup_valid',
type: 'boolean',
scope: 'process',
description: 'Whether backup was successful',
defaultValue: false
},
{
name: 'log_result',
type: 'object',
scope: 'process',
description: 'Result of logging operation',
defaultValue: {}
}
],
nodes: [
{
id: 'start-5',
type: 'start',
position: { x: 100, y: 500 },
data: {
label: 'Start',
description: 'Begin automated backup process'
}
},
{
id: 'api-3',
type: 'api',
position: { x: 300, y: 500 },
data: {
label: 'Backup Database',
description: 'Perform database backup',
apiMethod: 'POST',
apiUrl: '/api/backup/database',
requestBody: '{"timestamp": "{{current_timestamp}}"}',
outputVariable: 'backup_result'
}
},
{
id: 'business-rule-4',
type: 'business-rule',
position: { x: 500, y: 500 },
data: {
label: 'Validate Backup',
description: 'Validate backup completion',
ruleGroups: [
{
name: 'Backup Success',
conditions: [
{
variable: 'backup_result.status',
operator: 'eq',
value: 'success',
minValue: null,
maxValue: null
}
],
actions: [
{ type: 'set_variable', variable: 'backup_valid', value: 'true' }
]
}
]
}
},
{
id: 'api-4',
type: 'api',
position: { x: 700, y: 500 },
data: {
label: 'Update Backup Log',
description: 'Log backup status',
apiMethod: 'POST',
apiUrl: '/api/logs/backup',
requestBody: '{"status": "{{backup_result.status}}", "timestamp": "{{current_timestamp}}"}',
outputVariable: 'log_result'
}
},
{
id: 'notification-10',
type: 'notification',
position: { x: 900, y: 500 },
data: {
label: 'Notify Admin',
description: 'Send backup status to administrator',
notificationType: 'info',
recipientType: 'role',
recipientRole: 'system_admin',
subject: 'Daily backup completed',
message: 'Automated database backup has completed successfully.',
priority: 'low'
}
},
{
id: 'end-5',
type: 'end',
position: { x: 1100, y: 500 },
data: {
label: 'End',
description: 'Backup process completed'
}
}
],
edges: [
{ id: 'e34', source: 'start-5', target: 'api-3', sourceHandle: 'start-5-bottom', targetHandle: 'api-3-top', type: 'smoothstep' },
{ id: 'e35', source: 'api-3', target: 'business-rule-4', sourceHandle: 'api-3-bottom', targetHandle: 'business-rule-4-top', type: 'smoothstep' },
{ id: 'e36', source: 'business-rule-4', target: 'api-4', sourceHandle: 'business-rule-4-bottom', targetHandle: 'api-4-top', type: 'smoothstep' },
{ id: 'e37', source: 'api-4', target: 'notification-10', sourceHandle: 'api-4-bottom', targetHandle: 'notification-10-top', type: 'smoothstep' },
{ id: 'e38', source: 'notification-10', target: 'end-5', sourceHandle: 'notification-10-bottom', targetHandle: 'end-5-top', type: 'smoothstep' }
]
},
{
id: 'basic-linear',
name: 'Basic Linear Process',
description: 'Simple start-to-end process for learning the basics.',
category: 'approval',
complexity: 'Simple',
nodeCount: 4,
edgeCount: 3,
variables: [
{
name: 'user_email',
type: 'string',
scope: 'process',
description: 'Email address of the user submitting the form',
defaultValue: ''
},
{
name: 'submission_id',
type: 'string',
scope: 'process',
description: 'Unique identifier for the form submission',
defaultValue: ''
}
],
nodes: [
{
id: 'start-6',
type: 'start',
position: { x: 100, y: 600 },
data: {
label: 'Start',
description: 'Process begins'
}
},
{
id: 'form-10',
type: 'form',
position: { x: 300, y: 600 },
data: {
label: 'Input Form',
description: 'Collect user input',
formId: null,
formName: 'Basic Input Form'
}
},
{
id: 'notification-11',
type: 'notification',
position: { x: 500, y: 600 },
data: {
label: 'Send Notification',
description: 'Send confirmation notification',
notificationType: 'success',
recipientType: 'variable',
recipientVariable: 'user_email',
subject: 'Form submitted successfully',
message: 'Your form has been submitted successfully.',
priority: 'medium'
}
},
{
id: 'end-6',
type: 'end',
position: { x: 700, y: 600 },
data: {
label: 'End',
description: 'Process completed'
}
}
],
edges: [
{ id: 'e39', source: 'start-6', target: 'form-10', sourceHandle: 'start-6-bottom', targetHandle: 'form-10-top', type: 'smoothstep' },
{ id: 'e40', source: 'form-10', target: 'notification-11', sourceHandle: 'form-10-bottom', targetHandle: 'notification-11-top', type: 'smoothstep' },
{ id: 'e41', source: 'notification-11', target: 'end-6', sourceHandle: 'notification-11-bottom', targetHandle: 'end-6-top', type: 'smoothstep' }
]
}
];
// Filtered templates based on active category
const filteredTemplates = computed(() => {
if (activeCategory.value === 'all') {
return templates;
}
return templates.filter(template => template.category === activeCategory.value);
});
// Handle template selection
function useTemplate(template) {
emit('select-template', template);
showModal.value = false;
}
// Preview template
function previewTemplate(template) {
selectedTemplate.value = template;
showPreviewModal.value = true;
}
// Use template from preview
function useTemplateFromPreview() {
if (selectedTemplate.value) {
emit('select-template', selectedTemplate.value);
showPreviewModal.value = false;
showModal.value = false;
}
}
// Get icon based on template category/type
const getTemplateIcon = (template) => {
const icons = {
'approval': 'material-symbols:check-circle-outline',
'onboarding': 'material-symbols:person-add-outline',
'support': 'material-symbols:support-agent',
'business': 'material-symbols:business-center-outline',
'automation': 'material-symbols:precision-manufacturing-outline'
};
return icons[template.category] || 'material-symbols:account-tree';
};
// Get icon for node types
const getNodeIcon = (nodeType) => {
const icons = {
'start': 'material-symbols:play-circle-outline',
'end': 'material-symbols:stop-circle-outline',
'form': 'material-symbols:description-outline',
'api': 'material-symbols:api',
'gateway': 'material-symbols:call-split',
'notification': 'material-symbols:notifications-outline',
'business-rule': 'material-symbols:rule',
'script': 'material-symbols:code',
'html': 'material-symbols:code',
'subprocess': 'material-symbols:hub-outline'
};
return icons[nodeType] || 'material-symbols:account-tree';
};
// Calculate edge path for preview
const getEdgePath = (edge) => {
const sourceNode = selectedTemplate.value?.nodes.find(n => n.id === edge.source);
const targetNode = selectedTemplate.value?.nodes.find(n => n.id === edge.target);
if (!sourceNode || !targetNode) return '';
// Normalize positions to fit within the preview container
const sourceX = Math.max(5, Math.min(95, (sourceNode.position.x / 2000) * 100));
const sourceY = Math.max(5, Math.min(95, (sourceNode.position.y / 800) * 100));
const targetX = Math.max(5, Math.min(95, (targetNode.position.x / 2000) * 100));
const targetY = Math.max(5, Math.min(95, (targetNode.position.y / 800) * 100));
// Calculate the distance and direction
const dx = targetX - sourceX;
const dy = targetY - sourceY;
const distance = Math.sqrt(dx * dx + dy * dy);
// Adjust target position to account for arrow marker
const arrowOffset = 3; // Adjust based on marker size
const targetAdjustedX = targetX - (dx / distance) * arrowOffset;
const targetAdjustedY = targetY - (dy / distance) * arrowOffset;
// Create a smooth curved path with proper control points
if (Math.abs(dx) > Math.abs(dy)) {
// Horizontal flow - use gentle curve
const controlX1 = sourceX + dx * 0.25;
const controlY1 = sourceY;
const controlX2 = sourceX + dx * 0.75;
const controlY2 = targetAdjustedY;
return `M ${sourceX} ${sourceY} C ${controlX1} ${controlY1}, ${controlX2} ${controlY2}, ${targetAdjustedX} ${targetAdjustedY}`;
} else {
// Vertical flow - use straight line with slight curve
const controlX1 = sourceX;
const controlY1 = sourceY + dy * 0.25;
const controlX2 = targetAdjustedX;
const controlY2 = sourceY + dy * 0.75;
return `M ${sourceX} ${sourceY} C ${controlX1} ${controlY1}, ${controlX2} ${controlY2}, ${targetAdjustedX} ${targetAdjustedY}`;
}
};
// Calculate arrow points for fallback arrows
const getArrowPoints = (edge) => {
const sourceNode = selectedTemplate.value?.nodes.find(n => n.id === edge.source);
const targetNode = selectedTemplate.value?.nodes.find(n => n.id === edge.target);
if (!sourceNode || !targetNode) return '';
// Normalize positions
const sourceX = Math.max(5, Math.min(95, (sourceNode.position.x / 2000) * 100));
const sourceY = Math.max(5, Math.min(95, (sourceNode.position.y / 800) * 100));
const targetX = Math.max(5, Math.min(95, (targetNode.position.x / 2000) * 100));
const targetY = Math.max(5, Math.min(95, (targetNode.position.y / 800) * 100));
// Calculate direction
const dx = targetX - sourceX;
const dy = targetY - sourceY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) return '';
// Normalize direction vector
const dirX = dx / distance;
const dirY = dy / distance;
// Arrow size
const arrowSize = 4;
// Arrow tip position (slightly before the target node)
const tipX = targetX - dirX * 6;
const tipY = targetY - dirY * 6;
// Arrow base points (perpendicular to direction)
const perpX = -dirY;
const perpY = dirX;
const base1X = tipX - dirX * arrowSize + perpX * arrowSize;
const base1Y = tipY - dirY * arrowSize + perpY * arrowSize;
const base2X = tipX - dirX * arrowSize - perpX * arrowSize;
const base2Y = tipY - dirY * arrowSize - perpY * arrowSize;
return `${tipX},${tipY} ${base1X},${base1Y} ${base2X},${base2Y}`;
};
// Vue Flow preview data
const previewFlow = ref(null);
// Computed properties for Vue Flow preview
const previewNodes = computed(() => {
if (!selectedTemplate.value?.nodes) return [];
return selectedTemplate.value.nodes.map(node => {
// Get default styling from ProcessBuilderComponents
const defaultStyling = getDefaultNodeStyling(node.type);
return {
...node,
data: {
...node.data,
// Ensure styling properties are included
backgroundColor: node.data.backgroundColor || defaultStyling.backgroundColor,
borderColor: node.data.borderColor || defaultStyling.borderColor,
textColor: node.data.textColor || defaultStyling.textColor,
shape: node.data.shape || defaultStyling.shape
}
};
});
});
const previewEdges = computed(() => {
if (!selectedTemplate.value?.edges) return [];
return selectedTemplate.value.edges.map(edge => ({
...edge,
type: 'smoothstep',
animated: false,
style: {
stroke: '#475569',
strokeWidth: 2,
strokeDasharray: 'none'
},
markerEnd: {
type: 'arrowclosed',
width: 20,
height: 20,
color: '#475569'
}
}));
});
// Get default styling for node types
const getDefaultNodeStyling = (nodeType) => {
const stylingMap = {
'start': {
backgroundColor: '#dcfce7',
borderColor: '#10b981',
textColor: '#065f46',
shape: 'circle'
},
'end': {
backgroundColor: '#fee2e2',
borderColor: '#dc2626',
textColor: '#991b1b',
shape: 'circle'
},
'form': {
backgroundColor: '#faf5ff',
borderColor: '#9333ea',
textColor: '#6b21a8',
shape: 'rectangle'
},
'api': {
backgroundColor: '#eff6ff',
borderColor: '#3b82f6',
textColor: '#1e40af',
shape: 'rectangle'
},
'gateway': {
backgroundColor: '#fff7ed',
borderColor: '#f97316',
textColor: '#c2410c',
shape: 'diamond'
},
'notification': {
backgroundColor: '#f0f9ff',
borderColor: '#0ea5e9',
textColor: '#0284c7',
shape: 'rectangle'
},
'business-rule': {
backgroundColor: '#fdf4ff',
borderColor: '#a855f7',
textColor: '#7c3aed',
shape: 'rectangle'
},
'script': {
backgroundColor: '#f9fafb',
borderColor: '#6b7280',
textColor: '#374151',
shape: 'rectangle'
},
'html': {
backgroundColor: '#e0f2fe',
borderColor: '#0ea5e9',
textColor: '#0c4a6e',
shape: 'rectangle'
},
'subprocess': {
backgroundColor: '#f0fdfa',
borderColor: '#14b8a6',
textColor: '#134e4a',
shape: 'rectangle'
}
};
return stylingMap[nodeType] || {
backgroundColor: '#ffffff',
borderColor: '#e2e8f0',
textColor: '#374151',
shape: 'rectangle'
};
};
</script>
<style scoped>
.process-templates-container {
max-height: 70vh;
overflow-y: auto;
padding: 1rem;
}
.template-card {
transition: all 0.3s ease;
cursor: pointer;
}
.template-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.template-preview {
height: 160px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.template-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border-radius: 50%;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 3px solid #e2e8f0;
transition: all 0.3s ease;
}
.template-card:hover .template-icon-wrapper {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.preview-btn {
backdrop-filter: blur(4px);
}
/* Preview Modal Styles */
.template-preview-container {
max-height: 80vh;
overflow-y: auto;
}
.process-flow-preview {
position: relative;
background: linear-gradient(45deg, #f8fafc 25%, transparent 25%),
linear-gradient(-45deg, #f8fafc 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f8fafc 75%),
linear-gradient(-45deg, transparent 75%, #f8fafc 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
border-radius: 8px;
overflow: hidden;
}
.process-flow-diagram {
position: relative;
width: 100%;
height: 100%;
min-height: 300px;
}
.preview-node {
position: absolute;
transform: translate(-50%, -50%);
z-index: 10;
}
.node-content {
display: flex;
align-items: center;
background: white;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 8px 12px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
font-size: 11px;
white-space: nowrap;
transition: all 0.2s ease;
min-width: 80px;
justify-content: center;
backdrop-filter: blur(4px);
}
.node-content:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
border-color: #3b82f6;
z-index: 20;
}
/* Node type specific styling */
.preview-node .node-content {
border-color: #e2e8f0;
background: white;
}
.preview-node .node-content[data-type="start"] {
border-color: #10b981;
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
color: #065f46;
}
.preview-node .node-content[data-type="end"] {
border-color: #ef4444;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
color: #991b1b;
}
.preview-node .node-content[data-type="gateway"] {
border-color: #f97316;
background: linear-gradient(135deg, #fff7ed 0%, #fed7aa 100%);
color: #c2410c;
border-radius: 50%;
width: 60px;
height: 60px;
min-width: 60px;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(45deg);
}
.preview-node .node-content[data-type="gateway"] > div {
transform: rotate(-45deg);
display: flex;
align-items: center;
justify-content: center;
}
.preview-node .node-content[data-type="form"] {
border-color: #9333ea;
background: linear-gradient(135deg, #faf5ff 0%, #e9d5ff 100%);
color: #6b21a8;
}
.preview-node .node-content[data-type="api"] {
border-color: #3b82f6;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
color: #1e40af;
}
.preview-node .node-content[data-type="notification"] {
border-color: #0ea5e9;
background: linear-gradient(135deg, #f0f9ff 0%, #bae6fd 100%);
color: #0284c7;
}
.preview-node .node-content[data-type="business-rule"] {
border-color: #a855f7;
background: linear-gradient(135deg, #fdf4ff 0%, #e9d5ff 100%);
color: #7c3aed;
}
.preview-node .node-content[data-type="script"] {
border-color: #6b7280;
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
color: #374151;
}
.variable-item {
transition: all 0.2s ease;
}
.variable-item:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}
/* Flow edge styling */
.flow-edge {
transition: all 0.3s ease;
}
.flow-edge:hover {
stroke-width: 3.5;
opacity: 1;
filter: brightness(1.2);
}
/* Vue Flow Preview Styling */
.preview-vue-flow {
width: 100%;
height: 100%;
}
.preview-vue-flow .vue-flow__node {
cursor: default;
}
.preview-vue-flow .vue-flow__node:hover {
transform: none;
}
.preview-vue-flow .vue-flow__controls {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.preview-vue-flow .vue-flow__minimap {
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.preview-vue-flow .vue-flow__background {
background-color: #f8fafc;
}
/* Enhanced Preview Node Styling */
.preview-node .node-content {
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-width: 2px;
border-style: solid;
}
.preview-node .node-content:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 20;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.template-card:hover {
transform: translateY(-2px);
}
.process-flow-preview {
height: 60vh;
}
.node-content {
font-size: 10px;
padding: 4px 8px;
}
}
</style>