diff --git a/README.md b/README.md
index d856891..ad53740 100644
--- a/README.md
+++ b/README.md
@@ -221,7 +221,7 @@ Gauge styles: `heat` (blue→amber→red), `load` (emerald→amber→red), `emer
Interactive task scheduling with Frappe Gantt. Split-pane layout: task grid
on top, SVG timeline chart on bottom. Drag to reschedule, resize to change
-duration, project-colored bars, dependency arrows.
+duration, sortable grid columns, project-colored bars, and pinned task styling.
```html
@@ -236,9 +236,9 @@ var gantt = SF.gantt.create({
viewMode: 'Quarter Day',
splitSizes: [40, 60],
columns: [
- { key: 'name', label: 'Task' },
- { key: 'start', label: 'Start' },
- { key: 'end', label: 'End' },
+ { key: 'name', label: 'Task', sortable: true },
+ { key: 'start', label: 'Start', sortable: true },
+ { key: 'end', label: 'End', sortable: true },
{ key: 'priority', label: 'P', render: function (t) {
return {
unsafeHtml: 'P' + t.priority + '',
@@ -258,6 +258,8 @@ gantt.setTasks([
start: '2026-03-15 09:00',
end: '2026-03-15 10:30',
priority: 1,
+ projectIndex: 0,
+ pinned: true,
custom_class: 'project-color-0 priority-1',
dependencies: '',
},
@@ -277,6 +279,7 @@ gantt.highlightTask('task-1');
```
View modes: `Quarter Day`, `Half Day`, `Day`, `Week`, `Month`.
+Sortable headers are opt-in per column via `sortable: true`.
## Backend Adapters
diff --git a/css-src/17-gantt-layout.css b/css-src/17-gantt-layout.css
index b6368a9..78f558f 100644
--- a/css-src/17-gantt-layout.css
+++ b/css-src/17-gantt-layout.css
@@ -94,15 +94,6 @@
border-radius: var(--sf-radius-sm);
}
-/* ── Maximized Pane ── */
-.sf-gantt-pane.maximized {
- position: absolute !important;
- inset: 0 !important;
- z-index: 50;
- height: 100% !important;
- width: 100% !important;
-}
-
/* ── Grid Table ── */
.sf-gantt-grid {
height: 100%;
diff --git a/css-src/18-gantt-bars.css b/css-src/18-gantt-bars.css
index b15795e..f7bc917 100644
--- a/css-src/18-gantt-bars.css
+++ b/css-src/18-gantt-bars.css
@@ -20,13 +20,6 @@
.gantt .bar-wrapper.priority-2 .bar { opacity: 0.85; }
.gantt .bar-wrapper.priority-3 .bar { opacity: 0.7; }
-/* Cross-project dependency arrows */
-.gantt .arrow path.cross-project-arrow {
- stroke: var(--sf-emerald-500);
- stroke-dasharray: 5 3;
- stroke-width: 1.4;
-}
-
/* Bar hover glow */
.gantt .bar-wrapper:hover .bar {
filter: brightness(1.12) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.22));
@@ -58,48 +51,6 @@
width 0.12s var(--sf-ease-out);
}
-/* Dragging state */
-.gantt .bar-wrapper.dragging {
- opacity: 0.88;
- filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.35));
- z-index: 100;
- transition: none !important;
-}
-
-.gantt .bar-wrapper.dragging .bar {
- stroke-width: 2;
- stroke: rgba(255, 255, 255, 0.7);
-}
-
-body.sf-gantt-dragging {
- cursor: grabbing !important;
- user-select: none !important;
-}
-
-body.sf-gantt-dragging * {
- cursor: grabbing !important;
-}
-
-/* ── Drag Tooltip ── */
-.sf-gantt-drag-tooltip {
- position: fixed;
- z-index: 9999;
- background: rgba(15, 23, 42, 0.92);
- color: #f1f5f9;
- font-size: 11.5px;
- font-family: var(--sf-font-mono);
- font-weight: 500;
- padding: 5px 10px;
- border-radius: 6px;
- pointer-events: none;
- white-space: nowrap;
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(4px);
- border: 1px solid rgba(255, 255, 255, 0.12);
- letter-spacing: 0.02em;
- display: none;
-}
-
/* ── Pinned Task Indicators ── */
.gantt .bar-wrapper.pinned .bar {
stroke: var(--sf-amber-500);
@@ -107,12 +58,6 @@ body.sf-gantt-dragging * {
stroke-dasharray: 6 3;
}
-.sf-gantt-pin-icon {
- color: var(--sf-amber-500);
- font-size: 11px;
- opacity: 0.85;
-}
-
/* ── Highlighted task (pulse from grid click) ── */
.gantt .bar-wrapper.highlighted .bar {
animation: sf-gantt-pulse 0.5s ease-in-out 3;
@@ -149,40 +94,6 @@ body.sf-gantt-dragging * {
color: var(--sf-gray-500);
}
-/* ── Project Badge (grid table) ── */
-.sf-project-badge {
- display: inline-flex;
- align-items: center;
- padding: var(--sf-badge-padding-y) var(--sf-badge-padding-x);
- padding-left: 20px;
- border-radius: var(--sf-badge-radius);
- font-size: var(--sf-badge-font-size);
- font-weight: var(--sf-badge-font-weight);
- position: relative;
- background: var(--sf-gray-100);
-}
-
-.sf-project-badge::before {
- content: '';
- position: absolute;
- left: 7px;
- top: 50%;
- transform: translateY(-50%);
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: currentColor;
-}
-
-.sf-project-badge.sf-project-0 { color: var(--sf-project-0); background: var(--sf-project-0-light); }
-.sf-project-badge.sf-project-1 { color: var(--sf-project-1); background: var(--sf-project-1-light); }
-.sf-project-badge.sf-project-2 { color: var(--sf-project-2); background: var(--sf-project-2-light); }
-.sf-project-badge.sf-project-3 { color: var(--sf-project-3); background: var(--sf-project-3-light); }
-.sf-project-badge.sf-project-4 { color: var(--sf-project-4); background: var(--sf-project-4-light); }
-.sf-project-badge.sf-project-5 { color: var(--sf-project-5); background: var(--sf-project-5-light); }
-.sf-project-badge.sf-project-6 { color: var(--sf-project-6); background: var(--sf-project-6-light); }
-.sf-project-badge.sf-project-7 { color: var(--sf-project-7); background: var(--sf-project-7-light); }
-
/* ── Gantt Popup ── */
.sf-gantt-popup {
padding: 8px 12px;
diff --git a/js-src/14-gantt.js b/js-src/14-gantt.js
index d7caec3..109fc60 100644
--- a/js-src/14-gantt.js
+++ b/js-src/14-gantt.js
@@ -21,6 +21,7 @@
var mountTarget = null;
var resizeObserver = null;
var tasks = [];
+ var sortState = { key: null, direction: 'asc' };
// ── Build DOM ──
var wrapper = sf.el('div', { className: 'sf-gantt-split' });
@@ -43,7 +44,6 @@
var chartHeader = sf.el('div', { className: 'sf-gantt-pane-header' });
chartHeader.appendChild(sf.el('h3', null, config.chartTitle || 'Timeline'));
- // View mode selector
var viewControls = sf.el('div', { className: 'sf-gantt-view-controls' });
var viewSelect = sf.el('select', { className: 'sf-gantt-view-select' });
var modes = [
@@ -59,9 +59,7 @@
viewSelect.appendChild(opt);
});
viewSelect.addEventListener('change', function () {
- if (ganttChart) {
- ganttChart.change_view_mode(viewSelect.value);
- }
+ if (ganttChart) ganttChart.change_view_mode(viewSelect.value);
});
viewControls.appendChild(viewSelect);
@@ -111,8 +109,7 @@
ctrl.refresh = function () {
if (ganttChart && tasks.length > 0) {
- var frappeTasks = tasksToFrappe(tasks);
- ganttChart.refresh(frappeTasks);
+ ganttChart.refresh(tasksToFrappe(tasks));
}
};
@@ -124,11 +121,9 @@
};
ctrl.highlightTask = function (taskId) {
- // Grid highlight
grid.querySelectorAll('.sf-gantt-row').forEach(function (row) {
row.classList.toggle('selected', row.dataset.taskId === taskId);
});
- // Bar highlight
var svg = chartContainer.querySelector('svg');
if (svg) {
svg.querySelectorAll('.bar-wrapper').forEach(function (bw) {
@@ -153,8 +148,6 @@
return ctrl;
- // ── Internal ──
-
function initSplit() {
if (typeof Split !== 'function') return;
if (splitInstance) {
@@ -224,12 +217,16 @@
return taskList
.filter(function (t) { return t.start && t.end; })
.map(function (t) {
+ var customClass = t.custom_class || '';
+ if (t.pinned) {
+ customClass = customClass ? customClass + ' pinned' : 'pinned';
+ }
return {
id: t.id,
name: t.name || t.label || t.id,
start: t.start,
end: t.end,
- custom_class: t.custom_class || '',
+ custom_class: customClass,
dependencies: t.dependencies || '',
};
});
@@ -273,32 +270,36 @@
function renderGrid(taskList) {
while (grid.firstChild) grid.removeChild(grid.firstChild);
var table = sf.el('table', { className: 'sf-gantt-table' });
-
- // Header
- var thead = sf.el('thead');
- var headerRow = sf.el('tr');
var columns = config.columns || [
{ key: 'name', label: 'Task' },
{ key: 'start', label: 'Start' },
{ key: 'end', label: 'End' },
];
+ var sortedTasks = sortTasks(taskList);
+
+ var thead = sf.el('thead');
+ var headerRow = sf.el('tr');
columns.forEach(function (col) {
- headerRow.appendChild(sf.el('th', null, col.label));
+ headerRow.appendChild(buildHeaderCell(col));
});
thead.appendChild(headerRow);
table.appendChild(thead);
- // Body
var tbody = sf.el('tbody');
- taskList.forEach(function (task) {
+ sortedTasks.forEach(function (task) {
+ var rowClasses = ['sf-gantt-row'];
+ if (task.custom_class) rowClasses.push(task.custom_class);
+ if (task.projectIndex != null) rowClasses.push('sf-project-' + task.projectIndex);
+
var tr = sf.el('tr', {
- className: 'sf-gantt-row' + (task.custom_class ? ' ' + task.custom_class : ''),
+ className: rowClasses.join(' '),
dataset: { taskId: task.id },
onClick: function () {
ctrl.highlightTask(task.id);
if (config.onTaskClick) config.onTaskClick(task);
},
});
+
columns.forEach(function (col) {
var td = sf.el('td');
if (col.key === 'name') {
@@ -316,12 +317,64 @@
}
tr.appendChild(td);
});
+
tbody.appendChild(tr);
});
table.appendChild(tbody);
grid.appendChild(table);
}
+ function buildHeaderCell(col) {
+ if (!col.sortable) {
+ return sf.el('th', null, col.label);
+ }
+
+ var isCurrent = sortState.key === col.key;
+ var th = sf.el('th', {
+ className: 'sortable' + (isCurrent ? ' active' : ''),
+ role: 'button',
+ tabIndex: 0,
+ 'aria-sort': isCurrent ? (sortState.direction === 'asc' ? 'ascending' : 'descending') : 'none',
+ });
+ th.appendChild(document.createTextNode(col.label));
+ th.appendChild(sf.el('span', { className: 'sort-icon' }, isCurrent ? (sortState.direction === 'asc' ? '▲' : '▼') : ''));
+
+ sf.bindActivation(th, function () {
+ if (sortState.key === col.key) {
+ sortState.direction = sortState.direction === 'asc' ? 'desc' : 'asc';
+ } else {
+ sortState.key = col.key;
+ sortState.direction = 'asc';
+ }
+ renderGrid(tasks);
+ });
+
+ return th;
+ }
+
+ function sortTasks(taskList) {
+ if (!sortState.key) return taskList.slice();
+ var sorted = taskList.slice();
+ sorted.sort(function (a, b) {
+ var aVal = sortValue(a[sortState.key], sortState.key);
+ var bVal = sortValue(b[sortState.key], sortState.key);
+ if (aVal === bVal) return 0;
+ if (sortState.direction === 'asc') return aVal < bVal ? -1 : 1;
+ return aVal > bVal ? -1 : 1;
+ });
+ return sorted;
+ }
+
+ function sortValue(value, key) {
+ if (value == null) return '';
+ if (key === 'start' || key === 'end') {
+ var parsed = Date.parse(value);
+ return isNaN(parsed) ? String(value).toLowerCase() : parsed;
+ }
+ if (typeof value === 'number') return value;
+ return String(value).toLowerCase();
+ }
+
function defaultPopup(task) {
var t = tasks.find(function (x) { return x.id === task.id; });
if (!t) return '';
diff --git a/static/sf/sf.css b/static/sf/sf.css
index 57a4cd4..d87d351 100644
--- a/static/sf/sf.css
+++ b/static/sf/sf.css
@@ -1706,15 +1706,6 @@ body {
border-radius: var(--sf-radius-sm);
}
-/* ── Maximized Pane ── */
-.sf-gantt-pane.maximized {
- position: absolute !important;
- inset: 0 !important;
- z-index: 50;
- height: 100% !important;
- width: 100% !important;
-}
-
/* ── Grid Table ── */
.sf-gantt-grid {
height: 100%;
@@ -1879,13 +1870,6 @@ body {
.gantt .bar-wrapper.priority-2 .bar { opacity: 0.85; }
.gantt .bar-wrapper.priority-3 .bar { opacity: 0.7; }
-/* Cross-project dependency arrows */
-.gantt .arrow path.cross-project-arrow {
- stroke: var(--sf-emerald-500);
- stroke-dasharray: 5 3;
- stroke-width: 1.4;
-}
-
/* Bar hover glow */
.gantt .bar-wrapper:hover .bar {
filter: brightness(1.12) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.22));
@@ -1917,48 +1901,6 @@ body {
width 0.12s var(--sf-ease-out);
}
-/* Dragging state */
-.gantt .bar-wrapper.dragging {
- opacity: 0.88;
- filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.35));
- z-index: 100;
- transition: none !important;
-}
-
-.gantt .bar-wrapper.dragging .bar {
- stroke-width: 2;
- stroke: rgba(255, 255, 255, 0.7);
-}
-
-body.sf-gantt-dragging {
- cursor: grabbing !important;
- user-select: none !important;
-}
-
-body.sf-gantt-dragging * {
- cursor: grabbing !important;
-}
-
-/* ── Drag Tooltip ── */
-.sf-gantt-drag-tooltip {
- position: fixed;
- z-index: 9999;
- background: rgba(15, 23, 42, 0.92);
- color: #f1f5f9;
- font-size: 11.5px;
- font-family: var(--sf-font-mono);
- font-weight: 500;
- padding: 5px 10px;
- border-radius: 6px;
- pointer-events: none;
- white-space: nowrap;
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(4px);
- border: 1px solid rgba(255, 255, 255, 0.12);
- letter-spacing: 0.02em;
- display: none;
-}
-
/* ── Pinned Task Indicators ── */
.gantt .bar-wrapper.pinned .bar {
stroke: var(--sf-amber-500);
@@ -1966,12 +1908,6 @@ body.sf-gantt-dragging * {
stroke-dasharray: 6 3;
}
-.sf-gantt-pin-icon {
- color: var(--sf-amber-500);
- font-size: 11px;
- opacity: 0.85;
-}
-
/* ── Highlighted task (pulse from grid click) ── */
.gantt .bar-wrapper.highlighted .bar {
animation: sf-gantt-pulse 0.5s ease-in-out 3;
@@ -2008,40 +1944,6 @@ body.sf-gantt-dragging * {
color: var(--sf-gray-500);
}
-/* ── Project Badge (grid table) ── */
-.sf-project-badge {
- display: inline-flex;
- align-items: center;
- padding: var(--sf-badge-padding-y) var(--sf-badge-padding-x);
- padding-left: 20px;
- border-radius: var(--sf-badge-radius);
- font-size: var(--sf-badge-font-size);
- font-weight: var(--sf-badge-font-weight);
- position: relative;
- background: var(--sf-gray-100);
-}
-
-.sf-project-badge::before {
- content: '';
- position: absolute;
- left: 7px;
- top: 50%;
- transform: translateY(-50%);
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: currentColor;
-}
-
-.sf-project-badge.sf-project-0 { color: var(--sf-project-0); background: var(--sf-project-0-light); }
-.sf-project-badge.sf-project-1 { color: var(--sf-project-1); background: var(--sf-project-1-light); }
-.sf-project-badge.sf-project-2 { color: var(--sf-project-2); background: var(--sf-project-2-light); }
-.sf-project-badge.sf-project-3 { color: var(--sf-project-3); background: var(--sf-project-3-light); }
-.sf-project-badge.sf-project-4 { color: var(--sf-project-4); background: var(--sf-project-4-light); }
-.sf-project-badge.sf-project-5 { color: var(--sf-project-5); background: var(--sf-project-5-light); }
-.sf-project-badge.sf-project-6 { color: var(--sf-project-6); background: var(--sf-project-6-light); }
-.sf-project-badge.sf-project-7 { color: var(--sf-project-7); background: var(--sf-project-7-light); }
-
/* ── Gantt Popup ── */
.sf-gantt-popup {
padding: 8px 12px;
diff --git a/static/sf/sf.js b/static/sf/sf.js
index 38df2f0..9b1196d 100644
--- a/static/sf/sf.js
+++ b/static/sf/sf.js
@@ -1380,6 +1380,7 @@ const SF = (function () {
var mountTarget = null;
var resizeObserver = null;
var tasks = [];
+ var sortState = { key: null, direction: 'asc' };
// ── Build DOM ──
var wrapper = sf.el('div', { className: 'sf-gantt-split' });
@@ -1402,7 +1403,6 @@ const SF = (function () {
var chartHeader = sf.el('div', { className: 'sf-gantt-pane-header' });
chartHeader.appendChild(sf.el('h3', null, config.chartTitle || 'Timeline'));
- // View mode selector
var viewControls = sf.el('div', { className: 'sf-gantt-view-controls' });
var viewSelect = sf.el('select', { className: 'sf-gantt-view-select' });
var modes = [
@@ -1418,9 +1418,7 @@ const SF = (function () {
viewSelect.appendChild(opt);
});
viewSelect.addEventListener('change', function () {
- if (ganttChart) {
- ganttChart.change_view_mode(viewSelect.value);
- }
+ if (ganttChart) ganttChart.change_view_mode(viewSelect.value);
});
viewControls.appendChild(viewSelect);
@@ -1470,8 +1468,7 @@ const SF = (function () {
ctrl.refresh = function () {
if (ganttChart && tasks.length > 0) {
- var frappeTasks = tasksToFrappe(tasks);
- ganttChart.refresh(frappeTasks);
+ ganttChart.refresh(tasksToFrappe(tasks));
}
};
@@ -1483,11 +1480,9 @@ const SF = (function () {
};
ctrl.highlightTask = function (taskId) {
- // Grid highlight
grid.querySelectorAll('.sf-gantt-row').forEach(function (row) {
row.classList.toggle('selected', row.dataset.taskId === taskId);
});
- // Bar highlight
var svg = chartContainer.querySelector('svg');
if (svg) {
svg.querySelectorAll('.bar-wrapper').forEach(function (bw) {
@@ -1512,8 +1507,6 @@ const SF = (function () {
return ctrl;
- // ── Internal ──
-
function initSplit() {
if (typeof Split !== 'function') return;
if (splitInstance) {
@@ -1583,12 +1576,16 @@ const SF = (function () {
return taskList
.filter(function (t) { return t.start && t.end; })
.map(function (t) {
+ var customClass = t.custom_class || '';
+ if (t.pinned) {
+ customClass = customClass ? customClass + ' pinned' : 'pinned';
+ }
return {
id: t.id,
name: t.name || t.label || t.id,
start: t.start,
end: t.end,
- custom_class: t.custom_class || '',
+ custom_class: customClass,
dependencies: t.dependencies || '',
};
});
@@ -1632,32 +1629,36 @@ const SF = (function () {
function renderGrid(taskList) {
while (grid.firstChild) grid.removeChild(grid.firstChild);
var table = sf.el('table', { className: 'sf-gantt-table' });
-
- // Header
- var thead = sf.el('thead');
- var headerRow = sf.el('tr');
var columns = config.columns || [
{ key: 'name', label: 'Task' },
{ key: 'start', label: 'Start' },
{ key: 'end', label: 'End' },
];
+ var sortedTasks = sortTasks(taskList);
+
+ var thead = sf.el('thead');
+ var headerRow = sf.el('tr');
columns.forEach(function (col) {
- headerRow.appendChild(sf.el('th', null, col.label));
+ headerRow.appendChild(buildHeaderCell(col));
});
thead.appendChild(headerRow);
table.appendChild(thead);
- // Body
var tbody = sf.el('tbody');
- taskList.forEach(function (task) {
+ sortedTasks.forEach(function (task) {
+ var rowClasses = ['sf-gantt-row'];
+ if (task.custom_class) rowClasses.push(task.custom_class);
+ if (task.projectIndex != null) rowClasses.push('sf-project-' + task.projectIndex);
+
var tr = sf.el('tr', {
- className: 'sf-gantt-row' + (task.custom_class ? ' ' + task.custom_class : ''),
+ className: rowClasses.join(' '),
dataset: { taskId: task.id },
onClick: function () {
ctrl.highlightTask(task.id);
if (config.onTaskClick) config.onTaskClick(task);
},
});
+
columns.forEach(function (col) {
var td = sf.el('td');
if (col.key === 'name') {
@@ -1675,12 +1676,64 @@ const SF = (function () {
}
tr.appendChild(td);
});
+
tbody.appendChild(tr);
});
table.appendChild(tbody);
grid.appendChild(table);
}
+ function buildHeaderCell(col) {
+ if (!col.sortable) {
+ return sf.el('th', null, col.label);
+ }
+
+ var isCurrent = sortState.key === col.key;
+ var th = sf.el('th', {
+ className: 'sortable' + (isCurrent ? ' active' : ''),
+ role: 'button',
+ tabIndex: 0,
+ 'aria-sort': isCurrent ? (sortState.direction === 'asc' ? 'ascending' : 'descending') : 'none',
+ });
+ th.appendChild(document.createTextNode(col.label));
+ th.appendChild(sf.el('span', { className: 'sort-icon' }, isCurrent ? (sortState.direction === 'asc' ? '▲' : '▼') : ''));
+
+ sf.bindActivation(th, function () {
+ if (sortState.key === col.key) {
+ sortState.direction = sortState.direction === 'asc' ? 'desc' : 'asc';
+ } else {
+ sortState.key = col.key;
+ sortState.direction = 'asc';
+ }
+ renderGrid(tasks);
+ });
+
+ return th;
+ }
+
+ function sortTasks(taskList) {
+ if (!sortState.key) return taskList.slice();
+ var sorted = taskList.slice();
+ sorted.sort(function (a, b) {
+ var aVal = sortValue(a[sortState.key], sortState.key);
+ var bVal = sortValue(b[sortState.key], sortState.key);
+ if (aVal === bVal) return 0;
+ if (sortState.direction === 'asc') return aVal < bVal ? -1 : 1;
+ return aVal > bVal ? -1 : 1;
+ });
+ return sorted;
+ }
+
+ function sortValue(value, key) {
+ if (value == null) return '';
+ if (key === 'start' || key === 'end') {
+ var parsed = Date.parse(value);
+ return isNaN(parsed) ? String(value).toLowerCase() : parsed;
+ }
+ if (typeof value === 'number') return value;
+ return String(value).toLowerCase();
+ }
+
function defaultPopup(task) {
var t = tasks.find(function (x) { return x.id === task.id; });
if (!t) return '';
diff --git a/tests/instance-local-dom.test.js b/tests/instance-local-dom.test.js
index 95b0857..5abbb5f 100644
--- a/tests/instance-local-dom.test.js
+++ b/tests/instance-local-dom.test.js
@@ -263,3 +263,47 @@ test('gantt initSplit keeps accepting scalar splitMinSize values', () => {
assert.equal(splitCalls[0].options.minSize[0], 160);
assert.equal(splitCalls[0].options.minSize[1], 160);
});
+
+test('gantt sortable columns render and reorder grid rows without throwing', () => {
+ const { SF } = loadSf(['js-src/00-core.js', 'js-src/14-gantt.js']);
+
+ const gantt = SF.gantt.create({
+ columns: [
+ { key: 'name', label: 'Task', sortable: true },
+ { key: 'start', label: 'Start' },
+ ],
+ });
+
+ gantt.setTasks([
+ { id: 'b', name: 'Beta', start: '2026-03-22', end: '2026-03-23' },
+ { id: 'a', name: 'Alpha', start: '2026-03-21', end: '2026-03-22' },
+ ]);
+
+ const header = gantt.el.querySelector('th');
+ header.click();
+
+ const rows = gantt.el.querySelectorAll('.sf-gantt-row');
+ assert.equal(rows[0].dataset.taskId, 'a');
+ assert.equal(rows[1].dataset.taskId, 'b');
+});
+
+test('gantt pinned tasks propagate pinned custom class to chart tasks', () => {
+ let seenTasks = null;
+
+ const { SF } = loadSf(['js-src/00-core.js', 'js-src/14-gantt.js'], {
+ Gantt: function (_selector, tasks) {
+ seenTasks = tasks;
+ return {
+ change_view_mode() {},
+ refresh() {},
+ };
+ },
+ });
+
+ const gantt = SF.gantt.create({});
+ gantt.setTasks([
+ { id: 'task-1', start: '2026-03-21', end: '2026-03-22', pinned: true, custom_class: 'critical' },
+ ]);
+
+ assert.equal(seenTasks[0].custom_class, 'critical pinned');
+});