Files
panel/resources/js/pages/dashboards/gantt.vue

1339 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
const props = defineProps({
initialTasks: {
type: Object,
default: () => ({
data: [],
links: []
})
},
config: {
type: Object,
default: () => ({})
}
})
const ganttContainer = ref(null)
const showDeleteModal = ref(false)
const showEditModal = ref(false)
const showAddModal = ref(false)
const currentTaskId = ref(null)
const currentTask = ref(null)
const taskForm = ref({
text: '',
start_date: '',
duration: 3,
progress: 0
})
const tasks = props.initialTasks.data.length > 0 ? props.initialTasks : {
data: [
{
id: 1,
text: "Office Itinerary",
start_date: "01-08-2025",
duration: 17,
progress: 0.95,
open: true
},
{
id: 11,
text: "Office facing",
start_date: "01-08-2025",
duration: 5,
parent: 1,
progress: 1
},
{
id: 111,
text: "Interior office",
start_date: "01-08-2025",
duration: 3,
parent: 11,
progress: 1
},
{
id: 112,
text: "Air conditioner check",
start_date: "05-08-2025",
duration: 2,
parent: 11,
progress: 1
},
{
id: 12,
text: "Furniture installation",
start_date: "08-08-2025",
duration: 2,
parent: 1,
progress: 1
},
{
id: 13,
text: "Employee relocation",
start_date: "10-08-2025",
duration: 8,
parent: 1,
progress: 0.67,
open: true
},
{
id: 131,
text: "Preparing workplaces",
start_date: "10-08-2025",
duration: 3,
parent: 13,
progress: 1
},
{
id: 132,
text: "Workplaces importation",
start_date: "13-08-2025",
duration: 3,
parent: 13,
progress: 1
},
{
id: 133,
text: "Workplaces exportation",
start_date: "16-08-2025",
duration: 2,
parent: 13,
progress: 0
},
{
id: 2,
text: "Product launch",
start_date: "01-08-2025",
duration: 18,
progress: 0.73,
open: true
},
{
id: 21,
text: "Perform initial testing",
start_date: "01-08-2025",
duration: 5,
parent: 2,
progress: 1
},
{
id: 22,
text: "Development",
start_date: "03-08-2025",
duration: 16,
parent: 2,
progress: 0.68,
open: true
},
{
id: 221,
text: "Develop System",
start_date: "03-08-2025",
duration: 5,
parent: 22,
progress: 1
},
{
id: 222,
text: "Beta Release",
start_date: "08-08-2025",
duration: 1,
parent: 22,
progress: 1
},
{
id: 223,
text: "Integrate System",
start_date: "09-08-2025",
duration: 4,
parent: 22,
progress: 1
},
{
id: 224,
text: "Test",
start_date: "13-08-2025",
duration: 3,
parent: 22,
progress: 0.67
},
{
id: 225,
text: "Marketing",
start_date: "16-08-2025",
duration: 3,
parent: 22,
progress: 0
},
{
id: 23,
text: "Analysis",
start_date: "01-08-2025",
duration: 4,
parent: 2,
progress: 1
},
{
id: 24,
text: "Design",
start_date: "06-08-2025",
duration: 6,
parent: 2,
progress: 0.75,
open: true
},
{
id: 241,
text: "Design database",
start_date: "06-08-2025",
duration: 4,
parent: 24,
progress: 1
},
{
id: 242,
text: "Software design",
start_date: "10-08-2025",
duration: 2,
parent: 24,
progress: 0.5
}
],
links: [
{ id: 1, source: 111, target: 112, type: "0" },
{ id: 2, source: 112, target: 12, type: "0" },
{ id: 3, source: 12, target: 13, type: "0" },
{ id: 4, source: 131, target: 132, type: "0" },
{ id: 5, source: 132, target: 133, type: "0" },
{ id: 6, source: 221, target: 222, type: "0" },
{ id: 7, source: 222, target: 223, type: "0" },
{ id: 8, source: 223, target: 224, type: "0" },
{ id: 9, source: 224, target: 225, type: "0" },
{ id: 10, source: 23, target: 24, type: "0" },
{ id: 11, source: 241, target: 242, type: "0" }
]
}
const initGantt = () => {
gantt.config.date_format = "%d-%m-%Y"
gantt.config.fit_tasks = true
gantt.config.start_date = null
gantt.config.end_date = null
gantt.config.columns = [
{ name: "wbs", label: "WBS", width: 50, align: "center" },
{ name: "text", label: "TASK NAME", width: 250, tree: true },
{ name: "start_date", label: "START TIME", width: 100, align: "center" },
{ name: "duration", label: "DURATION", width: 80, align: "center" }
]
gantt.config.scale_unit = "day"
gantt.config.date_scale = "%d %M"
gantt.config.scale_height = 50
gantt.config.row_height = 40
gantt.config.bar_height = 24
gantt.config.grid_width = 450
gantt.config.subscales = [
{ unit: "month", step: 1, date: "%M %Y" },
{ unit: "day", step: 1, date: "%j" }
]
gantt.config.drag_move = true
gantt.config.drag_resize = true
gantt.config.drag_progress = true
gantt.config.drag_links = true
gantt.config.details_on_create = true
gantt.config.details_on_dblclick = true
gantt.templates.task_class = function (start, end, task) {
if (task.progress === 1) return "completed-task"
if (task.progress >= 0.75) return "high-progress-task"
if (task.progress >= 0.25) return "medium-progress-task"
return "low-progress-task"
}
gantt.templates.grid_row_class = function (start, end, task) {
if (task.$level === 0) return "summary-row"
return ""
}
gantt.templates.rightside_text = function (start, end, task) {
if (task.progress === 1) {
return "<span class='completed-mark'>✓</span>"
}
return Math.round(task.progress * 100) + "%"
}
let taskCounter = 1
gantt.templates.grid_cell_value = function (item, column) {
if (column === "wbs") {
if (item.$level === 0) {
return taskCounter++
} else if (item.$level === 1) {
let parentIndex = gantt.getParent(item.id)
let parent = gantt.getTask(parentIndex)
let siblings = gantt.getChildren(parentIndex)
let childIndex = siblings.indexOf(item.id) + 1
return parent.$wbs + "." + childIndex
} else if (item.$level === 2) {
let parentIndex = gantt.getParent(item.id)
let parent = gantt.getTask(parentIndex)
let siblings = gantt.getChildren(parentIndex)
let childIndex = siblings.indexOf(item.id) + 1
return parent.$wbs + "." + childIndex
}
}
return gantt.templates.grid_cell_value_default(item, column)
}
gantt.attachEvent("onTaskLoading", function (task) {
if (task.$level === 0) {
task.$wbs = taskCounter
taskCounter++
} else if (task.$level === 1) {
let parent = gantt.getTask(gantt.getParent(task.id))
let siblings = gantt.getChildren(gantt.getParent(task.id))
let index = siblings.indexOf(task.id) + 1
task.$wbs = parent.$wbs + "." + index
} else if (task.$level === 2) {
let parentId = gantt.getParent(task.id)
let parent = gantt.getTask(parentId)
let siblings = gantt.getChildren(parentId)
let index = siblings.indexOf(task.id) + 1
task.$wbs = parent.$wbs + "." + index
}
return true
})
gantt.attachEvent("onContextMenu", function(taskId, linkId, e) {
if (taskId) {
showContextMenu(e, taskId)
return false
}
return true
})
gantt.init(ganttContainer.value)
gantt.parse(tasks)
}
const showContextMenu = (e, taskId) => {
hideContextMenu()
const contextMenu = document.createElement('div')
contextMenu.id = 'gantt-context-menu'
contextMenu.className = 'gantt-context-menu'
contextMenu.innerHTML = `
<div class="context-menu-item" onclick="window.ganttComponent.addSubTask('${taskId}')">
<svg class="menu-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M12 5v14m-7-7h14"/>
</svg>
Add Sub Task
</div>
<div class="context-menu-item" onclick="window.ganttComponent.editTask('${taskId}')">
<svg class="menu-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Edit Task
</div>
<div class="context-menu-item" onclick="window.ganttComponent.deleteTaskFromContext('${taskId}')">
<svg class="menu-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M3 6h18"/>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
</svg>
Delete Task
</div>
<div class="context-menu-separator"></div>
<div class="context-menu-item" onclick="window.ganttComponent.markTaskComplete('${taskId}')">
<svg class="menu-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<polyline points="9,11 12,14 22,4"/>
<path d="M21,12v7a2,2 0 0,1 -2,2H5a2,2 0 0,1 -2,-2V5a2,2 0 0,1 2,-2h11"/>
</svg>
Toggle Complete
</div>
`
contextMenu.style.position = 'fixed'
contextMenu.style.left = e.clientX + 'px'
contextMenu.style.top = e.clientY + 'px'
contextMenu.style.zIndex = '9999'
document.body.appendChild(contextMenu)
document.addEventListener('click', hideContextMenu)
window.ganttComponent = {
addSubTask,
editTask,
deleteTaskFromContext,
markTaskComplete
}
}
const hideContextMenu = () => {
const existingMenu = document.getElementById('gantt-context-menu')
if (existingMenu) {
existingMenu.remove()
}
document.removeEventListener('click', hideContextMenu)
}
const addSubTask = (parentId) => {
currentTaskId.value = parentId
taskForm.value = {
text: 'New Sub Task',
start_date: new Date().toISOString().split('T')[0],
duration: 3,
progress: 0
}
showAddModal.value = true
hideContextMenu()
}
const editTask = (taskId) => {
currentTaskId.value = taskId
const task = gantt.getTask(taskId)
currentTask.value = task
taskForm.value = {
text: task.text,
start_date: gantt.date.date_to_str("%Y-%m-%d")(task.start_date),
duration: task.duration,
progress: task.progress
}
showEditModal.value = true
hideContextMenu()
}
const deleteTaskFromContext = (taskId) => {
currentTaskId.value = taskId
currentTask.value = gantt.getTask(taskId)
showDeleteModal.value = true
hideContextMenu()
}
const markTaskComplete = (taskId) => {
let task = gantt.getTask(taskId)
task.progress = task.progress === 1 ? 0 : 1
gantt.updateTask(taskId)
gantt.render()
hideContextMenu()
}
const addTask = () => {
currentTaskId.value = null
taskForm.value = {
text: 'New Task',
start_date: new Date().toISOString().split('T')[0],
duration: 3,
progress: 0
}
showAddModal.value = true
}
const deleteTask = () => {
const selectedTask = gantt.getSelectedId()
if (selectedTask) {
currentTaskId.value = selectedTask
currentTask.value = gantt.getTask(selectedTask)
showDeleteModal.value = true
} else {
showNotification('Please select a task to delete', 'warning')
}
}
const confirmDelete = () => {
if (currentTaskId.value) {
gantt.deleteTask(currentTaskId.value)
showDeleteModal.value = false
currentTaskId.value = null
currentTask.value = null
showNotification('Task deleted successfully', 'success')
}
}
const saveTask = () => {
if (!taskForm.value.text.trim()) {
showNotification('Task name is required', 'error')
return
}
const taskData = {
text: taskForm.value.text,
start_date: gantt.date.str_to_date("%Y-%m-%d")(taskForm.value.start_date),
duration: parseInt(taskForm.value.duration),
progress: parseFloat(taskForm.value.progress)
}
if (showEditModal.value) {
Object.assign(currentTask.value, taskData)
gantt.updateTask(currentTaskId.value)
gantt.render()
showEditModal.value = false
showNotification('Task updated successfully', 'success')
} else if (showAddModal.value) {
const newTaskId = gantt.uid()
const newTask = {
id: newTaskId,
...taskData
}
if (currentTaskId.value) {
newTask.parent = currentTaskId.value
}
gantt.addTask(newTask)
gantt.refreshData()
gantt.selectTask(newTaskId)
gantt.showTask(newTaskId)
showAddModal.value = false
showNotification('Task added successfully', 'success')
}
resetForm()
}
const resetForm = () => {
currentTaskId.value = null
currentTask.value = null
taskForm.value = {
text: '',
start_date: '',
duration: 3,
progress: 0
}
}
const closeModal = () => {
showDeleteModal.value = false
showEditModal.value = false
showAddModal.value = false
resetForm()
}
const showNotification = (message, type = 'info') => {
const notification = document.createElement('div')
notification.className = `notification notification-${type}`
notification.innerHTML = `
<div class="notification-content">
<span class="notification-message">${message}</span>
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
`
document.body.appendChild(notification)
setTimeout(() => {
if (notification.parentElement) {
notification.remove()
}
}, 4000)
}
const markCompleted = () => {
const selectedTask = gantt.getSelectedId()
if (selectedTask) {
let task = gantt.getTask(selectedTask)
task.progress = task.progress === 1 ? 0 : 1
gantt.updateTask(selectedTask)
gantt.render()
showNotification(
`Task marked as ${task.progress === 1 ? 'completed' : 'incomplete'}`,
'success'
)
} else {
showNotification('Please select a task to mark as completed', 'warning')
}
}
onMounted(() => {
initGantt()
// Prevent GridStack drag when interacting with Gantt chart
const ganttElement = ganttContainer.value
if (ganttElement) {
ganttElement.addEventListener('mousedown', (e) => {
e.stopPropagation()
})
ganttElement.addEventListener('dragstart', (e) => {
e.stopPropagation()
})
ganttElement.addEventListener('drag', (e) => {
e.stopPropagation()
})
}
})
onBeforeUnmount(() => {
if (gantt.$container) {
gantt.clearAll()
}
})
</script>
<template>
<div class="gantt-container no-grid-drag">
<div class="gantt-toolbar">
<button @click="addTask" class="btn btn-primary">
<svg class="btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M12 5v14m-7-7h14"/>
</svg>
Add Task
</button>
<button @click="deleteTask" class="btn btn-danger">
<svg class="btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M3 6h18"/>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
</svg>
Delete Selected
</button>
<button @click="markCompleted" class="btn btn-success">
<svg class="btn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<polyline points="9,11 12,14 22,4"/>
<path d="M21,12v7a2,2 0 0,1 -2,2H5a2,2 0 0,1 -2,-2V5a2,2 0 0,1 2,-2h11"/>
</svg>
Toggle Complete
</button>
</div>
<div ref="ganttContainer" class="gantt-chart no-grid-drag"></div>
<div v-if="showDeleteModal" class="modal-overlay" @click="closeModal">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
<svg class="modal-icon delete-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M3 6h18"/>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
</svg>
Delete Task
</h3>
<button class="modal-close" @click="closeModal">×</button>
</div>
<div class="modal-body">
<p class="modal-message">
Are you sure you want to delete <strong>"{{ currentTask?.text }}"</strong>?
</p>
<p class="modal-warning">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeModal">Cancel</button>
<button class="btn btn-danger" @click="confirmDelete">Delete</button>
</div>
</div>
</div>
<div v-if="showEditModal" class="modal-overlay" @click="closeModal">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
<svg class="modal-icon edit-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Edit Task
</h3>
<button class="modal-close" @click="closeModal">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="saveTask">
<div class="form-group">
<label for="taskName">Task Name</label>
<input
id="taskName"
v-model="taskForm.text"
type="text"
class="form-input"
placeholder="Enter task name"
required
>
</div>
<div class="form-row">
<div class="form-group">
<label for="startDate">Start Date</label>
<input
id="startDate"
v-model="taskForm.start_date"
type="date"
class="form-input"
required
>
</div>
<div class="form-group">
<label for="duration">Duration (days)</label>
<input
id="duration"
v-model="taskForm.duration"
type="number"
class="form-input"
min="1"
required
>
</div>
</div>
<div class="form-group">
<label for="progress">Progress (%)</label>
<div class="progress-input-container">
<input
id="progress"
v-model="taskForm.progress"
type="range"
class="form-range"
min="0"
max="1"
step="0.01"
>
<span class="progress-value">{{ Math.round(taskForm.progress * 100) }}%</span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeModal">Cancel</button>
<button class="btn btn-primary" @click="saveTask">Save Changes</button>
</div>
</div>
</div>
<div v-if="showAddModal" class="modal-overlay" @click="closeModal">
<div class="modal-container" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
<svg class="modal-icon add-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M12 5v14m-7-7h14"/>
</svg>
Add New Task
</h3>
<button class="modal-close" @click="closeModal">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="saveTask">
<div class="form-group">
<label for="newTaskName">Task Name</label>
<input
id="newTaskName"
v-model="taskForm.text"
type="text"
class="form-input"
placeholder="Enter task name"
required
>
</div>
<div class="form-row">
<div class="form-group">
<label for="newStartDate">Start Date</label>
<input
id="newStartDate"
v-model="taskForm.start_date"
type="date"
class="form-input"
required
>
</div>
<div class="form-group">
<label for="newDuration">Duration (days)</label>
<input
id="newDuration"
v-model="taskForm.duration"
type="number"
class="form-input"
min="1"
required
>
</div>
</div>
<div class="form-group">
<label for="newProgress">Progress (%)</label>
<div class="progress-input-container">
<input
id="newProgress"
v-model="taskForm.progress"
type="range"
class="form-range"
min="0"
max="1"
step="0.01"
>
<span class="progress-value">{{ Math.round(taskForm.progress * 100) }}%</span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="closeModal">Cancel</button>
<button class="btn btn-primary" @click="saveTask">Add Task</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.gantt-container {
width: 100%;
height: 650px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.gantt-toolbar {
padding: 12px 16px;
background: #fafbfc;
border-bottom: 1px solid #e5e7eb;
display: flex;
gap: 8px;
}
.btn {
display: inline-flex;
align-items: center;
padding: 8px 14px;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.15s ease;
gap: 6px;
}
.btn-icon {
flex-shrink: 0;
}
.btn-primary {
background: #0969da;
color: white;
border-color: #0969da;
}
.btn-primary:hover {
background: #0860ca;
border-color: #0860ca;
}
.btn-danger {
background: #d1242f;
color: white;
border-color: #d1242f;
}
.btn-danger:hover {
background: #b91c1c;
border-color: #b91c1c;
}
.btn-success {
background: #1a7f37;
color: white;
border-color: #1a7f37;
}
.btn-success:hover {
background: #166a2e;
border-color: #166a2e;
}
.btn-secondary {
background: #f6f8fa;
color: #24292f;
border-color: #d1d9e0;
}
.btn-secondary:hover {
background: #f3f4f6;
border-color: #c7d2da;
}
.gantt-chart {
height: calc(100% - 60px);
min-height: 500px;
}
</style>
<style>
.gantt_container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
}
.gantt_grid_head_cell,
.gantt_grid_data .gantt_cell {
border-right: 1px solid #e1e5e9;
border-bottom: 1px solid #e1e5e9;
}
.gantt_grid_head_cell {
background: #f6f8fa;
font-weight: 600;
text-align: center;
padding: 10px 8px;
color: #24292f;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.gantt_cell {
padding: 8px;
vertical-align: middle;
line-height: 1.4;
color: #24292f;
}
.gantt_tree_content {
padding-left: 4px;
}
.gantt_tree_icon {
width: 14px;
height: 14px;
margin-right: 4px;
opacity: 0.7;
}
.gantt_task_line.completed-task {
background: #1f883d;
border: 1px solid #1a7f37;
border-radius: 3px;
}
.gantt_task_line.high-progress-task {
background: #0969da;
border: 1px solid #0860ca;
border-radius: 3px;
}
.gantt_task_line.medium-progress-task {
background: #bf8700;
border: 1px solid #9a6700;
border-radius: 3px;
}
.gantt_task_line.low-progress-task {
background: #d1242f;
border: 1px solid #b91c1c;
border-radius: 3px;
}
.summary-row {
background: #f6f8fa;
font-weight: 600;
}
.gantt_task_progress {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
.gantt_scale_cell {
border-bottom: 1px solid #e1e5e9;
border-right: 1px solid #e1e5e9;
background: #f6f8fa;
font-weight: 500;
color: #24292f;
font-size: 11px;
}
.gantt_task_line {
border-radius: 3px;
height: 20px;
}
.gantt_side_content {
color: #656d76;
font-size: 11px;
margin-top: 2px;
font-weight: 500;
}
.completed-mark {
color: #1f883d;
font-weight: bold;
font-size: 12px;
}
.gantt_grid_data .gantt_row:nth-child(odd) {
background: #fafbfc;
}
.gantt_grid_data .gantt_row:hover {
background: #f3f4f6;
}
.gantt_selected .gantt_cell {
background: #ddf4ff;
color: #24292f;
}
.gantt_task_link {
stroke: #656d76;
stroke-width: 1.5;
}
.gantt_task_link .gantt_link_arrow {
fill: #656d76;
}
.gantt_milestone {
background: #8250df;
border: 2px solid #6f42c1;
}
.gantt-context-menu {
background: #ffffff;
border: 1px solid #d1d9e0;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2);
padding: 4px;
min-width: 180px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
z-index: 9999;
animation: contextMenuFadeIn 0.15s ease-out;
}
@keyframes contextMenuFadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.context-menu-item {
display: flex;
align-items: center;
padding: 6px 8px;
cursor: pointer;
color: #24292f;
transition: background-color 0.1s ease;
border-radius: 3px;
font-size: 13px;
line-height: 1.4;
user-select: none;
}
.context-menu-item:hover {
background: #f3f4f6;
}
.context-menu-item:active {
background: #eaeef2;
}
.menu-icon {
margin-right: 8px;
opacity: 0.8;
flex-shrink: 0;
color: #656d76;
}
.context-menu-separator {
height: 1px;
background: #d1d9e0;
margin: 4px 0;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(31, 35, 40, 0.5);
backdrop-filter: blur(3px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: modalOverlayFadeIn 0.2s ease-out;
}
@keyframes modalOverlayFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-container {
background: #ffffff;
border: 1px solid #d1d9e0;
border-radius: 8px;
box-shadow: 0 16px 32px rgba(31, 35, 40, 0.15);
width: 90%;
max-width: 480px;
max-height: 80vh;
overflow: hidden;
animation: modalSlideIn 0.2s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-16px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #d1d9e0;
background: #f6f8fa;
}
.modal-title {
display: flex;
align-items: center;
margin: 0;
font-size: 16px;
font-weight: 600;
color: #24292f;
}
.modal-icon {
margin-right: 8px;
flex-shrink: 0;
}
.delete-icon {
color: #d1242f;
}
.edit-icon {
color: #0969da;
}
.add-icon {
color: #1f883d;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #656d76;
cursor: pointer;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
transition: all 0.1s ease;
}
.modal-close:hover {
background: #eaeef2;
color: #24292f;
}
.modal-body {
padding: 20px;
}
.modal-message {
font-size: 14px;
color: #24292f;
margin: 0 0 6px 0;
line-height: 1.5;
}
.modal-warning {
font-size: 12px;
color: #656d76;
margin: 0;
}
.form-group {
margin-bottom: 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 600;
color: #24292f;
font-size: 12px;
}
.form-input {
width: 100%;
padding: 6px 8px;
border: 1px solid #d1d9e0;
border-radius: 4px;
font-size: 13px;
color: #24292f;
background: #ffffff;
transition: border-color 0.1s ease;
box-sizing: border-box;
}
.form-input:focus {
outline: none;
border-color: #0969da;
box-shadow: 0 0 0 2px rgba(9, 105, 218, 0.1);
}
.form-input:invalid {
border-color: #d1242f;
}
.progress-input-container {
display: flex;
align-items: center;
gap: 10px;
}
.form-range {
flex: 1;
height: 4px;
background: #d1d9e0;
border-radius: 2px;
outline: none;
}
.form-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #0969da;
border-radius: 50%;
cursor: pointer;
border: 2px solid #ffffff;
box-shadow: 0 1px 3px rgba(31, 35, 40, 0.2);
}
.form-range::-moz-range-thumb {
width: 16px;
height: 16px;
background: #0969da;
border-radius: 50%;
cursor: pointer;
border: 2px solid #ffffff;
box-shadow: 0 1px 3px rgba(31, 35, 40, 0.2);
}
.progress-value {
min-width: 40px;
text-align: right;
font-weight: 600;
color: #0969da;
font-size: 12px;
}
.modal-footer {
padding: 12px 20px 16px;
display: flex;
gap: 8px;
justify-content: flex-end;
background: #f6f8fa;
border-top: 1px solid #d1d9e0;
}
.notification {
position: fixed;
top: 16px;
right: 16px;
min-width: 280px;
background: #ffffff;
border: 1px solid #d1d9e0;
border-radius: 6px;
box-shadow: 0 8px 24px rgba(31, 35, 40, 0.1);
z-index: 10001;
animation: notificationSlideIn 0.2s ease-out;
}
@keyframes notificationSlideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.notification-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
}
.notification-message {
font-size: 13px;
color: #24292f;
font-weight: 500;
}
.notification-close {
background: none;
border: none;
font-size: 16px;
color: #656d76;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
transition: all 0.1s ease;
}
.notification-close:hover {
background: #f3f4f6;
color: #24292f;
}
.notification-success {
border-left: 3px solid #1f883d;
}
.notification-error {
border-left: 3px solid #d1242f;
}
.notification-warning {
border-left: 3px solid #bf8700;
}
.notification-info {
border-left: 3px solid #0969da;
}
</style>