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

1292 lines
32 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>
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
export default {
name: 'GanttChart',
data() {
return {
showDeleteModal: false,
showEditModal: false,
showAddModal: false,
currentTaskId: null,
currentTask: null,
taskForm: {
text: '',
start_date: '',
duration: 3,
progress: 0
},
tasks: {
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" }
]
}
}
},
mounted() {
this.initGantt()
},
beforeUnmount() {
if (gantt.$container) {
gantt.clearAll()
}
},
methods: {
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) {
this.showContextMenu(e, taskId)
return false
}
return true
}.bind(this))
gantt.init(this.$refs.ganttContainer)
gantt.parse(this.tasks)
},
showContextMenu(e, taskId) {
this.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', this.hideContextMenu)
window.ganttComponent = this
},
hideContextMenu() {
const existingMenu = document.getElementById('gantt-context-menu')
if (existingMenu) {
existingMenu.remove()
}
document.removeEventListener('click', this.hideContextMenu)
},
addSubTask(parentId) {
this.currentTaskId = parentId
this.taskForm = {
text: 'New Sub Task',
start_date: new Date().toISOString().split('T')[0],
duration: 3,
progress: 0
}
this.showAddModal = true
this.hideContextMenu()
},
editTask(taskId) {
this.currentTaskId = taskId
const task = gantt.getTask(taskId)
this.currentTask = task
this.taskForm = {
text: task.text,
start_date: gantt.date.date_to_str("%Y-%m-%d")(task.start_date),
duration: task.duration,
progress: task.progress
}
this.showEditModal = true
this.hideContextMenu()
},
deleteTaskFromContext(taskId) {
this.currentTaskId = taskId
this.currentTask = gantt.getTask(taskId)
this.showDeleteModal = true
this.hideContextMenu()
},
markTaskComplete(taskId) {
let task = gantt.getTask(taskId)
task.progress = task.progress === 1 ? 0 : 1
gantt.updateTask(taskId)
gantt.render()
this.hideContextMenu()
},
addTask() {
this.currentTaskId = null
this.taskForm = {
text: 'New Task',
start_date: new Date().toISOString().split('T')[0],
duration: 3,
progress: 0
}
this.showAddModal = true
},
deleteTask() {
const selectedTask = gantt.getSelectedId()
if (selectedTask) {
this.currentTaskId = selectedTask
this.currentTask = gantt.getTask(selectedTask)
this.showDeleteModal = true
} else {
this.showNotification('Please select a task to delete', 'warning')
}
},
confirmDelete() {
if (this.currentTaskId) {
gantt.deleteTask(this.currentTaskId)
this.showDeleteModal = false
this.currentTaskId = null
this.currentTask = null
this.showNotification('Task deleted successfully', 'success')
}
},
saveTask() {
if (!this.taskForm.text.trim()) {
this.showNotification('Task name is required', 'error')
return
}
const taskData = {
text: this.taskForm.text,
start_date: gantt.date.str_to_date("%Y-%m-%d")(this.taskForm.start_date),
duration: parseInt(this.taskForm.duration),
progress: parseFloat(this.taskForm.progress)
}
if (this.showEditModal) {
Object.assign(this.currentTask, taskData)
gantt.updateTask(this.currentTaskId)
gantt.render()
this.showEditModal = false
this.showNotification('Task updated successfully', 'success')
} else if (this.showAddModal) {
const newTaskId = gantt.uid()
const newTask = {
id: newTaskId,
...taskData
}
if (this.currentTaskId) {
newTask.parent = this.currentTaskId
}
gantt.addTask(newTask)
gantt.refreshData()
gantt.selectTask(newTaskId)
gantt.showTask(newTaskId)
this.showAddModal = false
this.showNotification('Task added successfully', 'success')
}
this.resetForm()
},
resetForm() {
this.currentTaskId = null
this.currentTask = null
this.taskForm = {
text: '',
start_date: '',
duration: 3,
progress: 0
}
},
closeModal() {
this.showDeleteModal = false
this.showEditModal = false
this.showAddModal = false
this.resetForm()
},
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)
},
markCompleted() {
const selectedTask = gantt.getSelectedId()
if (selectedTask) {
let task = gantt.getTask(selectedTask)
task.progress = task.progress === 1 ? 0 : 1
gantt.updateTask(selectedTask)
gantt.render()
this.showNotification(
`Task marked as ${task.progress === 1 ? 'completed' : 'incomplete'}`,
'success'
)
} else {
this.showNotification('Please select a task to mark as completed', 'warning')
}
}
}
}
</script>
<template>
<div class="gantt-container">
<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"></div>
<!-- Delete Modal -->
<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>
<!-- Edit Modal -->
<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>
<!-- Add Modal -->
<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>