Popular features/Split tasks
Loading live demo…from dhtmlxcode.com
Popular features
Split tasks
Cut a task into segments with a scissors tool where work pauses, keeping the pieces on one row with a labeled gap between them.
gantt.plugins({ tooltip: true });
gantt.config.date_format = "%Y-%m-%d";
gantt.config.work_time = true;
gantt.config.row_height = 36;
gantt.config.bar_height = 24;
gantt.config.scale_height = 52;
gantt.config.min_column_width = 34;
gantt.config.drag_links = false; // no link handles: this demo is about splitting, not linking
gantt.config.open_split_tasks = true;
/* ---------------------------------------------------------------------------
* Anchor the sprint to next Monday so the demo never looks stale.
* ------------------------------------------------------------------------ */
function mondayOnOrAfter(date) {
date = gantt.date.day_start(new Date(date));
while (date.getDay() !== 1) date = gantt.date.add(date, 1, "day");
return date;
}
let PROJECT_START = mondayOnOrAfter(new Date());
function wd(n) { return gantt.date.add(PROJECT_START, n, "day"); }
let PAUSE_DAYS = 4; // how far a cut pushes the second session out
gantt.config.fit_tasks = true;
gantt.config.scales = [
{ unit: "month", step: 1, format: "%F %Y" },
{ unit: "day", step: 1, format: "%j %D" }
];
gantt.templates.scale_cell_class = function (date) { return gantt.isWorkTime(date) ? "" : "weekend"; };
gantt.templates.timeline_cell_class = function (task, date) { return gantt.isWorkTime({ date, task }) ? "" : "weekend"; };
/* A split parent carries its own centred label that would ghost through the
gaps, so it stays blank. Every child segment gets a chronological number,
while regular tasks keep their normal text. */
function isSplit(task) { return task.render === "split"; }
function getSplitParent(task) {
let parentId = gantt.getParent(task.id);
if (!parentId || !gantt.isTaskExists(parentId)) return null;
let parent = gantt.getTask(parentId);
return isSplit(parent) ? parent : null;
}
function getOrderedSegments(parentId) {
return gantt.getChildren(parentId)
.map(function (id) { return gantt.getTask(id); })
.sort(function (a, b) {
let byStart = a.start_date - b.start_date;
return byStart || (a.end_date - b.end_date);
});
}
function getSegmentNumber(task) {
let parent = getSplitParent(task);
if (!parent) return 0;
let segments = getOrderedSegments(parent.id);
for (let i = 0; i < segments.length; i++) {
if (segments[i].id == task.id) return i + 1;
}
return 0;
}
gantt.templates.task_text = function (start, end, task) {
if (isSplit(task)) return "";
let segmentNumber = getSegmentNumber(task);
return segmentNumber ? "#" + segmentNumber : task.text;
};
gantt.templates.grid_row_class = function (start, end, task) { return isSplit(task) ? "split_row" : ""; };
/* ---------------------------------------------------------------------------
* Pause reasons -- the picker drives the label a new interruption gets.
* ------------------------------------------------------------------------ */
let REASONS = [
{ key: "vacation", label: "Vacation" },
{ key: "oncall", label: "On-call" },
{ key: "hotfix", label: "Hotfix" },
{ key: "training", label: "Training" },
{ key: "leave", label: "Parental leave" },
{ key: "break", label: "Sprint break" }
];
let CAUSE_LABELS = {};
REASONS.forEach(function (r) { CAUSE_LABELS[r.key] = r.label; });
let currentReason = "oncall";
function buildReasonMenu() {
document.getElementById("reason_menu").innerHTML = REASONS.map(function (r) {
return '<div class="dhx_menu__item dhx_menu__item--check' + (r.key === currentReason ? " checked" : "") +
'" onclick="pickReason(\'' + r.key + '\')">' + r.label + '</div>';
}).join("");
}
function pickReason(key) {
currentReason = key;
DHX.ui.setText("reason_label", CAUSE_LABELS[key]);
buildReasonMenu();
DHX.dd.closeAll();
}
/* ---------------------------------------------------------------------------
* GRID
* ------------------------------------------------------------------------ */
function ownerCell(task) { return task.owner ? DHX.ui.escape(task.owner) : ""; }
function pausedCell(task) {
if (!isSplit(task)) return "";
let key = task.cause || "break";
return '<span class="cause_chip cause_' + key + '"><span class="cause_dot"></span>' + (CAUSE_LABELS[key] || "Paused") + '</span>';
}
gantt.config.columns = [
{ name: "text", label: "Task", tree: true, width: 230, resize: true, editor: { type: "text", map_to: "text" } },
{ name: "owner", label: "Owner", align: "left", width: 130, resize: true, template: ownerCell },
{ name: "paused", label: "Paused for", align: "center", width: 132, resize: true, template: pausedCell },
{ name: "add", width: 40, resize: true }
];
let dateToStr = gantt.date.date_to_str("%d %M");
gantt.templates.tooltip_text = function (start, end, task) {
let splitParent = getSplitParent(task);
let segmentNumber = splitParent ? getSegmentNumber(task) : 0;
let name = splitParent ? splitParent.text + " (#" + segmentNumber + ")" : task.text;
let html = "<b>" + DHX.ui.escape(name || "") + "</b>";
if (task.owner) html += "<br>" + DHX.ui.escape(task.owner);
if (isSplit(task) && task.cause) html += "<br>Paused for " + (CAUSE_LABELS[task.cause] || "");
html += "<br>" + dateToStr(start) + " - " + dateToStr(end);
return html;
};
/* ---------------------------------------------------------------------------
* DATA: two sprint swimlanes. Two tasks start out interrupted so the pause
* pill is visible on load; the rest are continuous, waiting for the scissors.
* ------------------------------------------------------------------------ */
function buildData() {
return {
tasks: [
{ id: 1, text: "Sprint 24: Checkout & Payments Overhaul", type: "project", open: true },
{ id: 10, text: "Sprint planning & backlog grooming", parent: 1, start_date: wd(1), duration: 1, progress: 1, owner: "Whole team" },
/* pre-interrupted: two sessions around a vacation */
{ id: 20, text: "Search filters redesign", parent: 1, type: "project", render: "split", open: false, cause: "vacation", owner: "Priya Shah" },
{ id: 21, text: "Search filters redesign", parent: 20, start_date: wd(1), duration: 3, progress: 0.4, owner: "Priya Shah" },
{ id: 22, text: "", parent: 20, start_date: wd(11), duration: 4, progress: 0, owner: "Priya Shah" },
{ id: 40, text: "Checkout API refactor", parent: 1, start_date: wd(2), duration: 8, progress: 0.25, owner: "Elena Volkov" },
{ id: 44, text: "Coupon engine rework", parent: 1, start_date: wd(4), duration: 6, progress: 0.1, owner: "Priya Shah" },
{ id: 45, text: "Cart persistence bugfix", parent: 1, start_date: wd(9), duration: 4, progress: 0, owner: "Noah Kim" },
{ id: 62, text: "Address autocomplete", parent: 1, start_date: wd(12), duration: 5, progress: 0, owner: "Noah Kim" },
{ id: 60, text: "Update API documentation",parent: 1, start_date: wd(16), duration: 3, progress: 0, owner: "Jordan Lee" },
{ id: 2, text: "Sprint 24: Trust & Safety", type: "project", open: true },
/* pre-interrupted: two sessions around a training week */
{ id: 50, text: "Fraud detection rules engine", parent: 2, type: "project", render: "split", open: false, cause: "training", owner: "Sam Okafor" },
{ id: 51, text: "Fraud detection rules engine", parent: 50, start_date: wd(7), duration: 3, progress: 0.35, owner: "Sam Okafor" },
{ id: 52, text: "", parent: 50, start_date: wd(16), duration: 4, progress: 0, owner: "Sam Okafor" },
{ id: 30, text: "Payment gateway integration", parent: 2, start_date: wd(3), duration: 7, progress: 0.2, owner: "Marcus Webb" },
{ id: 33, text: "Bot mitigation tuning", parent: 2, start_date: wd(6), duration: 6, progress: 0, owner: "Sam Okafor" },
{ id: 36, text: "Audit log pipeline", parent: 2, start_date: wd(13), duration: 5, progress: 0, owner: "Dana Ruiz" },
{ id: 70, text: "Regression test pass", parent: 2, start_date: wd(22), duration: 4, progress: 0, owner: "QA Guild" },
{ id: 95, text: "Security review sign-off", parent: 2, start_date: wd(25), duration: 2, progress: 0, owner: "Security Guild" },
{ id: 99, text: "Sprint 24 complete", parent: 2, type: "milestone", start_date: wd(30), duration: 0, progress: 0 },
{ id: 3, text: "Sprint 24: Platform & Infra", type: "project", open: true },
{ id: 80, text: "Infra capacity planning", parent: 3, start_date: wd(1), duration: 3, progress: 0.5, owner: "Dana Ruiz" },
/* pre-interrupted: two sessions around a hotfix */
{ id: 85, text: "CI pipeline upgrade", parent: 3, type: "project", render: "split", open: false, cause: "hotfix", owner: "Elena Volkov" },
{ id: 86, text: "CI pipeline upgrade", parent: 85, start_date: wd(3), duration: 3, progress: 0.4, owner: "Elena Volkov" },
{ id: 87, text: "", parent: 85, start_date: wd(10), duration: 3, progress: 0, owner: "Elena Volkov" },
{ id: 90, text: "Database migration dry-run", parent: 3, start_date: wd(7), duration: 6, progress: 0, owner: "Marcus Webb" },
{ id: 91, text: "Observability dashboards", parent: 3, start_date: wd(9), duration: 4, progress: 0, owner: "Jordan Lee" },
{ id: 92, text: "Load testing", parent: 3, start_date: wd(14), duration: 5, progress: 0, owner: "QA Guild" },
{ id: 93, text: "Cost optimization pass", parent: 3, start_date: wd(19), duration: 3, progress: 0, owner: "Dana Ruiz" },
{ id: 94, text: "Infra runbook updates", parent: 3, start_date: wd(23), duration: 3, progress: 0, owner: "Noah Kim" },
{ id: 96, text: "Platform review", parent: 3, start_date: wd(27), duration: 2, progress: 0, owner: "Security Guild" },
{ id: 98, text: "Platform ready", parent: 3, type: "milestone", start_date: wd(30), duration: 0, progress: 0 }
],
links: [
{ id: 1, source: 10, target: 40, type: "0" },
{ id: 2, source: 40, target: 45, type: "0" },
{ id: 3, source: 33, target: 70, type: "0" },
{ id: 4, source: 70, target: 95, type: "0" },
{ id: 5, source: 95, target: 99, type: "0" },
{ id: 6, source: 80, target: 90, type: "0" },
{ id: 7, source: 90, target: 92, type: "0" },
{ id: 8, source: 92, target: 93, type: "0" },
{ id: 9, source: 93, target: 94, type: "0" },
{ id: 10, source: 94, target: 96, type: "0" },
{ id: 11, source: 96, target: 98, type: "0" }
]
};
}
/* ---------------------------------------------------------------------------
* SCISSORS: a click cuts, while a drag still moves or resizes the segment.
* ------------------------------------------------------------------------ */
let scissorsMode = true; // the demo opens ready to cut
let suppressCutUntil = 0;
let hoverIdPosition = {id: null, pos: null};
function toggleScissors() {
scissorsMode = !scissorsMode;
applyScissorsMode();
}
/* The mode changes only click behaviour and the visual hint. Dragging and
resizing remain enabled regardless of the toggle state. */
function applyScissorsMode() {
DHX.toggle.setWrap("scissors_toggle", scissorsMode);
document.body.classList.toggle("scissors", scissorsMode);
showScissorsMessage();
}
function showScissorsMessage() {
gantt.message({
id: "scissors_info",
expire: -1,
text: scissorsMode
? "<b>Scissors are on.</b> Click a task bar to split it. Drag or resize the same bar to reschedule it; touching or overlapping sessions merge automatically."
: "<b>Scissors are off.</b> Drag or resize any session. Turn Scissors on when a regular click should cut the bar."
});
}
/* Update hover task ID */
gantt.attachEvent("onMouseMove", function (id, e){
if (!e.target.closest(".gantt_task_content")){
return
}
const previousId = hoverIdPosition.id;
hoverIdPosition.id = id || null
if (previousId !== hoverIdPosition.id && gantt.isTaskExists(previousId)){
gantt.refreshTask(previousId)
}
if (hoverIdPosition.id) {
hoverIdPosition.pos = gantt.utils.dom.getRelativeEventPosition(e, gantt.$task_data).x;
gantt.refreshTask(hoverIdPosition.id)
}
});
/* A drag normally finishes with a browser click. Keep a short guard after the
drag ends so that releasing a moved bar never cuts it accidentally. */
gantt.attachEvent("onAfterTaskDrag", function (id, mode) {
suppressCutUntil = Date.now() + 250;
if (mode === "move" || mode === "resize") {
mergeConnectedSegments(id);
}
});
gantt.attachEvent("onTaskClick", function (id, e) {
if (!scissorsMode) return true;
if (Date.now() < suppressCutUntil) return false;
/* onTaskClick also fires in the grid. Only a click on the timeline bar cuts. */
let bar = e.target.closest && e.target.closest(".gantt_task_line");
if (!bar) return true;
let task = gantt.getTask(id);
let x = gantt.utils.dom.getRelativeEventPosition(e, gantt.$task_data).x;
let cut = gantt.date.day_start(gantt.dateFromPos(x));
if (splitTaskAt(task, cut, currentReason)) gantt.render();
return false;
});
/* Cut `task` at `cutDate`, inserting a PAUSE_DAYS pause.
* - a plain leaf -> wrapped into a new split project (engine pattern)
* - a split segment -> that segment is cut, the tail becomes a new segment */
function splitTaskAt(task, cutDate, reason) {
if (gantt.hasChild(task.id)) return false; // swimlane / wrapper rows
if (task.type === gantt.config.types.milestone) return false;
let parentId = gantt.getParent(task.id);
let parent = parentId ? gantt.getTask(parentId) : null;
let start = task.start_date, end = task.end_date;
if (cutDate <= start || cutDate >= end) return false; // click must land inside the bar
if (parent && isSplit(parent)) {
/* already a split: shorten this segment, add the tail as a new session */
task.end_date = new Date(cutDate);
gantt.updateTask(task.id);
addSession(parent.id, gantt.date.add(cutDate, PAUSE_DAYS, "day"), gantt.date.add(end, PAUSE_DAYS, "day"), task.owner);
parent.cause = reason;
gantt.updateTask(parent.id);
return true;
}
/* plain leaf -> wrap into a split project; the original work is session one */
let proj = gantt.addTask({ text: task.text, type: "project", render: "split", open: false, owner: task.owner, cause: reason }, parentId);
gantt.moveTask(proj, gantt.getTaskIndex(task.id), parentId);
gantt.moveTask(task.id, 0, proj);
gantt.calculateTaskLevel(gantt.getTask(task.id));
let seg1 = gantt.getTask(task.id);
seg1.end_date = new Date(cutDate);
gantt.updateTask(seg1.id);
addSession(proj, gantt.date.add(cutDate, PAUSE_DAYS, "day"), gantt.date.add(end, PAUSE_DAYS, "day"), task.owner);
return true;
}
function addSession(parentId, start, end, owner) {
let seg = gantt.addTask({ text: "", start_date: new Date(start), end_date: new Date(end), owner: owner, progress: 0 }, parentId);
gantt.calculateTaskLevel(gantt.getTask(seg));
return seg;
}
/* Merge only sessions that belong to the same split parent. Independent tasks
keep their identity even if their dates happen to overlap. */
function mergeConnectedSegments(taskId) {
if (!gantt.isTaskExists(taskId)) return;
let task = gantt.getTask(taskId);
let parent = getSplitParent(task);
if (!parent) return;
gantt.batchUpdate(function () {
let segments = getOrderedSegments(parent.id);
if (segments.length < 2) return;
let current = segments[0];
for (let i = 1; i < segments.length; i++) {
let next = segments[i];
if (next.start_date <= current.end_date) {
mergeSegmentData(current, next);
redirectSegmentLinks(next.id, current.id);
gantt.updateTask(current.id);
gantt.deleteTask(next.id);
} else {
current = next;
}
}
if (gantt.isTaskExists(parent.id) && gantt.getChildren(parent.id).length === 1) {
unwrapSingleSegment(parent.id);
}
});
}
function redirectSegmentLinks(sourceId, targetId) {
gantt.getLinks().slice().forEach(function (link) {
let changed = false;
if (link.source == sourceId) {
link.source = targetId;
changed = true;
}
if (link.target == sourceId) {
link.target = targetId;
changed = true;
}
if (!changed) return;
if (link.source == link.target) gantt.deleteLink(link.id);
else gantt.updateLink(link.id);
});
}
function mergeSegmentData(target, source) {
let targetSpan = Math.max(0, target.end_date - target.start_date);
let sourceSpan = Math.max(0, source.end_date - source.start_date);
let completed = targetSpan * (target.progress || 0) + sourceSpan * (source.progress || 0);
if (source.start_date < target.start_date) target.start_date = new Date(source.start_date);
if (source.end_date > target.end_date) target.end_date = new Date(source.end_date);
let mergedSpan = Math.max(1, target.end_date - target.start_date);
target.progress = Math.min(1, completed / mergedSpan);
if (!target.owner && source.owner) target.owner = source.owner;
}
/* When every interruption has disappeared, remove the technical split wrapper
and turn the remaining segment back into a regular task. */
function unwrapSingleSegment(parentId) {
let parent = gantt.getTask(parentId);
let childId = gantt.getChildren(parentId)[0];
let child = gantt.getTask(childId);
let grandParentId = gantt.getParent(parentId);
let parentIndex = gantt.getTaskIndex(parentId);
child.text = parent.text;
child.owner = child.owner || parent.owner;
gantt.moveTask(child.id, parentIndex, grandParentId);
gantt.calculateTaskLevel(child);
gantt.updateTask(child.id);
gantt.deleteTask(parentId);
}
gantt.addTaskLayer(function (task) {
if (!scissorsMode || !hoverIdPosition.id || gantt.getState().drag_id) {
return
}
if (task.type === "project"){
if (task.render == "split"){
gantt.eachTask(function(child){
if (hoverIdPosition.id == child.id){
task = child;
}
}, task.id);
}
else {
return;
}
}
if (hoverIdPosition.id != task.id){
return
}
let preview = document.createElement("div");
preview.className = "split_cut_preview";
preview.setAttribute("data-task-id", task.id);
preview.innerHTML =
'<span class="split_cut_icon" aria-hidden="true">' +
'<svg viewBox="0 0 24 24"><circle cx="6" cy="7" r="3"/><circle cx="6" cy="17" r="3"/><path d="m8.7 8.4 10.3 6.1"/><path d="m8.7 15.6 10.3-6.1"/></svg>' +
'</span>' +
'<span class="split_cut_line"></span>';
let cutDate = gantt.date.day_start(gantt.dateFromPos(hoverIdPosition.pos));
const minCutDate = gantt.date.add(task.start_date, 1, "day");
if (cutDate < minCutDate){
cutDate = minCutDate;
}
let pos = gantt.getTaskPosition(task, task.start_date, task.end_date);
preview.style.left = gantt.posFromDate(cutDate) + "px";
preview.style.top = (pos.top - 10) + "px";
preview.style.height = (pos.height + 18) + "px";
preview.style.display = "block";
return preview;
});
/* ---------------------------------------------------------------------------
* Pause markers: a connector + labelled pill in each gap between sessions.
* Computed from the actual segment children, so it follows live edits too.
* ------------------------------------------------------------------------ */
gantt.addTaskLayer(function (task) {
if (!isSplit(task) || task.$open) return false;
let childIds = gantt.getChildren(task.id);
if (!childIds || childIds.length < 2) return false;
let kids = childIds.map(function (id) { return gantt.getTask(id); })
.sort(function (x, y) { return x.start_date - y.start_date; });
let wrap = document.createElement("div");
wrap.className = "gap_layer";
let drew = false;
for (let i = 0; i < kids.length - 1; i++) {
let gapStart = kids[i].end_date, gapEnd = kids[i + 1].start_date;
if (gapEnd - gapStart < 12 * 60 * 60 * 1000) continue;
let pos = gantt.getTaskPosition(task, gapStart, gapEnd);
let yMid = pos.top + (pos.rowHeight || gantt.config.row_height) / 2;
let conn = document.createElement("div");
conn.className = "gap_conn";
conn.style.left = pos.left + "px";
conn.style.width = pos.width + "px";
conn.style.top = yMid + "px";
wrap.appendChild(conn);
let pill = document.createElement("div");
pill.className = "gap_pill cause_" + (task.cause || "break");
pill.textContent = CAUSE_LABELS[task.cause] || "Paused";
pill.style.left = (pos.left + pos.width / 2) + "px";
pill.style.top = yMid + "px";
wrap.appendChild(pill);
drew = true;
}
return drew ? wrap : false;
});
/* ---------------------------------------------------------------------------
* Reset back to the authored example.
* ------------------------------------------------------------------------ */
function resetExample() {
gantt.clearAll();
gantt.parse(buildData());
}
DHX.dd.initClickOutside();
buildReasonMenu();
document.body.classList.add("scissors"); // scissors start on (see toggle default)
gantt.init("gantt_here");
gantt.parse(buildData());
applyScissorsMode();<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Split tasks: cut a task where the work stops | dhtmlxGantt</title>
<script src="../../codebase/dhtmlxgantt.js?v=10.0.0"></script>
<link rel="stylesheet" href="../../codebase/dhtmlxgantt.css?v=10.0.0">
<link rel="stylesheet" href="../common/demo-controls/dhx_controls.css">
<script src="../common/demo-controls/dhx_controls.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<style>
body { display: flex; flex-direction: column; }
#gantt_here { flex: 1 1 auto; min-height: 0; width: 100%; }
.gantt_task_line { border: none; border-radius: 7px; box-shadow: 0 1px 2px rgba(16,24,40,.18); }
.gantt_task_line .gantt_task_progress { border-radius: 7px 0 0 7px; }
.gantt_task_line.gantt_project { border-radius: 5px; }
.gantt_task_cell.weekend, .gantt_scale_cell.weekend { background-color: rgba(15,23,42,.04); }
.split_cut_preview {
position: absolute;
display: none;
width: 1px;
pointer-events: none;
z-index: 6;
}
.split_cut_line {
position: absolute;
left: 0;
top: 18px;
width: 2px;
height: calc(100% - 18px);
transform: translateX(-1px);
background: repeating-linear-gradient(
to bottom,
#c62a30 0,
#c62a30 5px,
transparent 5px,
transparent 9px
);
background-position: 0 0;
animation: split_cut_dash 420ms linear infinite;
filter: drop-shadow(0 0 1px rgba(255,255,255,.85));
}
.split_cut_icon {
position: absolute;
left: 0;
top: 0;
width: 18px;
height: 18px;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--dhx-panel);
color: #c62a30;
box-shadow: 0 1px 4px rgba(16,24,40,.24);
}
.split_cut_icon svg {
width: 12px;
height: 12px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
@keyframes split_cut_dash {
to { background-position: 0 9px; }
}
.gap_layer { position: absolute; left: 0; top: 0; pointer-events: none; z-index: 2; }
.gap_conn { position: absolute; height: 0; border-top: 2px dotted #b7c3d4; transform: translateY(-50%); }
.gap_pill {
position: absolute; transform: translate(-50%, -50%);
display: inline-flex; align-items: center; gap: 6px;
padding: 2px 9px; border-radius: 11px;
background: var(--dhx-surface); border: 1px solid var(--dhx-line);
box-shadow: 0 1px 2px rgba(16,24,40,.16);
font: 600 11px var(--dhx-font); color: var(--dhx-ink-2); white-space: nowrap;
}
.gap_pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; flex: none; }
.cause_vacation .cause_dot, .cause_vacation.gap_pill::before { background: #1d6fd6; }
.cause_oncall .cause_dot, .cause_oncall.gap_pill::before { background: #7c3aed; }
.cause_hotfix .cause_dot, .cause_hotfix.gap_pill::before { background: #c0392b; }
.cause_training .cause_dot, .cause_training.gap_pill::before { background: #b8740a; }
.cause_leave .cause_dot, .cause_leave.gap_pill::before { background: #1f8a4c; }
.cause_break .cause_dot, .cause_break.gap_pill::before { background: #64748b; }
.cause_chip { display: inline-flex; align-items: center; gap: 6px; font: 600 12px var(--dhx-font); color: var(--dhx-ink-2); }
.cause_chip .cause_dot { width: 8px; height: 8px; border-radius: 50%; }
.muted_dash { color: var(--dhx-muted); }
.split_row .gantt_tree_icon.gantt_open,
.split_row .gantt_tree_icon.gantt_close { visibility: hidden; pointer-events: none; }
.toolbar_label { font: 600 13px var(--dhx-font); color: var(--dhx-ink-2); }
.reason_label { color: var(--dhx-ink); }
</style>
</head>
<body>
<div class="dhx_header" style="justify-content:center; gap:14px">
<span class="toolbar_label">Sprint 24 plan</span>
<label class="dhx_toggle active" id="scissors_toggle" onclick="toggleScissors()">
<span>✂ Scissors</span><span class="dhx_toggle__track on"></span>
</label>
<div class="dhx_dd" id="reason_dd">
<button class="dhx_btn dhx_btn--surface" onclick="DHX.dd.toggle('reason_dd')">
<span>Pause reason: <b class="reason_label" id="reason_label">On-call</b></span>
<span class="dhx_caret"><svg class="dhx_ic dhx_ic--sm" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="dhx_menu" id="reason_menu"></div>
</div>
<button class="dhx_btn dhx_btn--surface" title="Restore the starting plan" onclick="resetExample()">Reset example</button>
</div>
<div id="gantt_here"></div>
</body>
</html>