jey_theme/jey_theme/public/js/jey_theme.js

1565 lines
65 KiB
JavaScript
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.

/* ==========================================================================
Jey Theme — Multi-theme system + Desktop Icons
========================================================================== */
/* --- Theme + full-width initialisation (runs immediately, before paint) --- */
(function () {
var DEFAULT_THEME = "chrome";
var saved = localStorage.getItem("jey-theme");
if (!saved) {
saved = DEFAULT_THEME;
localStorage.setItem("jey-theme", saved);
}
document.documentElement.setAttribute("data-jey-theme", saved);
// Default full-width on fresh install (user never toggled it)
if (localStorage.getItem("container_fullwidth") === null) {
localStorage.setItem("container_fullwidth", "true");
}
// Inject saved column widths immediately to avoid flash.
// Also store grid keys so we can set data-jey-grid attributes early.
var resizeStyle = document.createElement("style");
resizeStyle.id = "jey-col-resize-styles";
var rules = [];
var earlyGridKeys = [];
for (var i = 0; i < localStorage.length; i++) {
var k = localStorage.key(i);
if (k.indexOf("jey-col-widths:") === 0) {
var gridKey = k.substring(15);
earlyGridKeys.push(gridKey);
try {
var widths = JSON.parse(localStorage.getItem(k));
Object.keys(widths).forEach(function (fn) {
rules.push(
'[data-jey-grid="' + gridKey + '"] .grid-static-col[data-fieldname="' + fn + '"]' +
" { flex: 0 0 " + widths[fn] + "px !important; width: " + widths[fn] + "px !important; max-width: none !important; }"
);
});
} catch (e) {}
}
}
resizeStyle.textContent = rules.join("\n");
document.head.appendChild(resizeStyle);
// Set data-jey-grid on grids as soon as they appear so CSS rules apply immediately
if (earlyGridKeys.length) {
var earlyObs = new MutationObserver(function () {
earlyGridKeys.forEach(function (key) {
// key = "Sales Invoice:items" → fieldname = "items"
var parts = key.split(":");
if (parts.length < 2) return;
var tableField = parts[1];
var control = document.querySelector('.frappe-control[data-fieldname="' + tableField + '"]');
if (control) {
var grid = control.querySelector(".form-grid");
if (grid && !grid.dataset.jeyGrid) {
grid.dataset.jeyGrid = key;
}
}
});
});
earlyObs.observe(document.body, { childList: true, subtree: true });
}
})();
(function () {
"use strict";
/* ==================================================================
Per-theme icon sets
Each theme can define its own SVGs. Falls back to "chrome" if a
key is missing, then to FALLBACK.
================================================================== */
var SETTINGS_PATHS = '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.32 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>';
var HR_PATHS = '<path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="8.5" cy="7" r="4"/><line class="plus-v" x1="20" y1="8" x2="20" y2="14"/><line class="plus-h" x1="23" y1="11" x2="17" y2="11"/>';
var THEME_ICONS = {
/* ============================================================
CHROME — detailed line-art icons from the reference HTML
============================================================ */
chrome: {
buying: { cls: "ico-buying", paths: '<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/>' },
selling: { cls: "ico-selling", paths: '<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline class="arrow-head" points="16 7 22 7 22 13"/>' },
stock: { cls: "ico-stock", paths: '<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>' },
manufacturing: { cls: "ico-mfg", paths: '<g class="gear1"><circle cx="9" cy="12" r="3"/><path d="M9 9V4.5"/><path d="M6.5 10.5L3 8"/><path d="M6.5 13.5L3 16"/><path d="M9 15v4.5"/><path d="M11.5 10.5L15 8"/><path d="M11.5 13.5L15 16"/></g><g class="gear2"><circle cx="17" cy="8" r="2.5"/><path d="M17 5.5V3"/><path d="M19.2 7L21 5.5"/><path d="M19.2 9L21 10.5"/><path d="M14.8 7L13 5.5"/><path d="M14.8 9L13 10.5"/></g>' },
projects: { cls: "ico-proj", paths: '<rect class="col1" x="3" y="3" width="5" height="18" rx="1"/><rect class="col2" x="10" y="3" width="5" height="12" rx="1"/><rect class="col3" x="17" y="3" width="5" height="15" rx="1"/>' },
quality: { cls: "ico-quality", paths: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><polyline points="9 12 11 14 15 10"/>' },
assets: { cls: "ico-assets", paths: '<rect x="3" y="5" width="18" height="15" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/><circle class="lock-dot" cx="12" cy="15" r="2"/><line x1="12" y1="13" x2="12" y2="10"/><line x1="7" y1="5" x2="7" y2="2"/><line x1="17" y1="5" x2="17" y2="2"/>' },
subcontracting: { cls: "ico-sub", paths: '<g class="person-l"><circle cx="8" cy="7" r="3"/><path d="M7 11H4a2 2 0 00-2 2v1"/><path d="M11 18l-3-3 2-2 3 3"/></g><g class="person-r"><circle cx="16" cy="7" r="3"/><path d="M17 11h3a2 2 0 012 2v1"/><path d="M13 18l3-3-2-2-3 3"/></g>' },
organization: { cls: "ico-org", paths: '<rect x="4" y="2" width="16" height="20" rx="1"/><line x1="9" y1="6" x2="9" y2="6.01"/><line x1="15" y1="6" x2="15" y2="6.01"/><line x1="9" y1="10" x2="9" y2="10.01"/><line x1="15" y1="10" x2="15" y2="10.01"/><line x1="9" y1="14" x2="9" y2="14.01"/><line x1="15" y1="14" x2="15" y2="14.01"/><path d="M10 22v-4h4v4"/>' },
accounting: { cls: "ico-acc", paths: '<rect x="4" y="2" width="16" height="20" rx="2"/><rect x="7" y="5" width="10" height="4" rx="1"/><line class="acc-btn" x1="7" y1="13" x2="7" y2="13.01" stroke-width="2.5"/><line x1="12" y1="13" x2="12" y2="13.01" stroke-width="2.5"/><line class="acc-btn" x1="17" y1="13" x2="17" y2="13.01" stroke-width="2.5" style="animation-delay:0.4s"/><line x1="7" y1="17" x2="7" y2="17.01" stroke-width="2.5"/><line class="acc-btn" x1="12" y1="17" x2="12" y2="17.01" stroke-width="2.5" style="animation-delay:0.8s"/><line x1="17" y1="17" x2="17" y2="17.01" stroke-width="2.5"/>' },
framework: { cls: "ico-framework", paths: '<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>' },
"erpnext settings": { cls: "ico-settings", paths: SETTINGS_PATHS },
settings: { cls: "ico-settings", paths: SETTINGS_PATHS },
"accounts setup": { cls: "ico-settings", paths: SETTINGS_PATHS },
"account setup": { cls: "ico-settings", paths: SETTINGS_PATHS },
hr: { cls: "ico-hr", paths: HR_PATHS },
"hr module": { cls: "ico-hr", paths: HR_PATHS },
"frappe hr": { cls: "ico-hr", paths: HR_PATHS },
invoicing: { paths: '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/>' },
payments: { paths: '<rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/><line x1="5" y1="15" x2="9" y2="15"/><line x1="13" y1="15" x2="15" y2="15"/>' },
"financial reports": { paths: '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="16" y2="17"/><line x1="8" y1="9" x2="10" y2="9"/>' },
taxes: { paths: '<path d="M4 2h16a2 2 0 012 2v16a2 2 0 01-2 2H4a2 2 0 01-2-2V4a2 2 0 012-2z"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="9" y1="12" x2="15" y2="12"/><line x1="9" y1="16" x2="13" y2="16"/><line x1="6" y1="8" x2="6" y2="8.01" stroke-width="2"/><line x1="6" y1="12" x2="6" y2="12.01" stroke-width="2"/><line x1="6" y1="16" x2="6" y2="16.01" stroke-width="2"/>' },
"taxes az": { paths: '<path d="M4 2h16a2 2 0 012 2v16a2 2 0 01-2 2H4a2 2 0 01-2-2V4a2 2 0 012-2z"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="9" y1="12" x2="15" y2="12"/><line x1="9" y1="16" x2="13" y2="16"/><line x1="6" y1="8" x2="6" y2="8.01" stroke-width="2"/><line x1="6" y1="12" x2="6" y2="12.01" stroke-width="2"/><line x1="6" y1="16" x2="6" y2="16.01" stroke-width="2"/>' },
banking: { paths: '<path d="M3 21h18"/><path d="M3 10h18"/><path d="M12 3l9 7H3l9-7z"/><line x1="6" y1="10" x2="6" y2="21"/><line x1="10" y1="10" x2="10" y2="21"/><line x1="14" y1="10" x2="14" y2="21"/><line x1="18" y1="10" x2="18" y2="21"/>' },
budget: { paths: '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/>' },
"share management": { paths: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>' },
subscription: { paths: '<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><polyline points="7 10 10 13 17 7"/>' },
crm: { paths: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/>' },
support: { paths: '<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>' },
/* --- Framework sub-modules --- */
automation: { paths: '<polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/>' },
build: { paths: '<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>' },
data: { paths: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>' },
email: { paths: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22 6 12 13 2 6"/>' },
integrations: { paths: '<circle cx="12" cy="12" r="3"/><circle cx="19" cy="5" r="2"/><circle cx="5" cy="5" r="2"/><circle cx="19" cy="19" r="2"/><circle cx="5" cy="19" r="2"/><line x1="14.5" y1="9.5" x2="17.5" y2="6.5"/><line x1="9.5" y1="9.5" x2="6.5" y2="6.5"/><line x1="14.5" y1="14.5" x2="17.5" y2="17.5"/><line x1="9.5" y1="14.5" x2="6.5" y2="17.5"/>' },
printing: { paths: '<polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2"/><rect x="6" y="14" width="12" height="8"/>' },
system: { paths: '<rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><line x1="6" y1="8" x2="10" y2="8"/><line x1="6" y1="11" x2="14" y2="11"/>' },
users: { paths: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/>' },
website: { paths: '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>' },
/* --- HRMS sub-modules --- */
attendance: { paths: '<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><polyline points="9 16 11 18 15 14"/>' },
"shift & attendance": { paths: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>' },
"shift &amp; attendance": { paths: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>' },
leaves: { paths: '<path d="M6 3v18"/><path d="M6 8c6-2 10 2 16 0"/><path d="M6 15c6-2 10 2 16 0"/>' },
payroll: { paths: '<rect x="2" y="4" width="20" height="16" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="6" y1="7" x2="6" y2="7.01" stroke-width="2"/><line x1="6" y1="14" x2="9" y2="14"/><line x1="6" y1="17" x2="8" y2="17"/><line x1="15" y1="14" x2="18" y2="14"/><line x1="15" y1="17" x2="19" y2="17"/>' },
people: { paths: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/>' },
recruitment: { paths: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><path d="M11 8v6"/><path d="M8 11h6"/>' },
performance: { paths: '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>' },
expenses: { paths: '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/>' },
tenure: { paths: '<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="8" y1="14" x2="8" y2="14.01" stroke-width="2"/><line x1="12" y1="14" x2="12" y2="14.01" stroke-width="2"/><line x1="16" y1="14" x2="16" y2="14.01" stroke-width="2"/><line x1="8" y1="18" x2="8" y2="18.01" stroke-width="2"/>' },
"tax & benefits": { paths: '<path d="M4 2h16a2 2 0 012 2v16a2 2 0 01-2 2H4a2 2 0 01-2-2V4a2 2 0 012-2z"/><path d="M16 8l-8 8"/><circle cx="9" cy="9" r="1.5"/><circle cx="15" cy="15" r="1.5"/>' },
"tax &amp; benefits": { paths: '<path d="M4 2h16a2 2 0 012 2v16a2 2 0 01-2 2H4a2 2 0 01-2-2V4a2 2 0 012-2z"/><path d="M16 8l-8 8"/><circle cx="9" cy="9" r="1.5"/><circle cx="15" cy="15" r="1.5"/>' },
_fallback: '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/>'
},
/* ============================================================
MODERN — minimal geometric icons, visually distinct
============================================================ */
modern: {
buying: { paths: '<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/>' },
selling: { paths: '<line x1="12" y1="20" x2="12" y2="10"/><polyline points="18 14 12 20 6 14"/><line x1="4" y1="4" x2="20" y2="4"/>' },
stock: { paths: '<rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/>' },
manufacturing: { paths: '<circle cx="12" cy="12" r="3"/><path d="M12 1v4"/><path d="M12 19v4"/><path d="M4.22 4.22l2.83 2.83"/><path d="M16.95 16.95l2.83 2.83"/><path d="M1 12h4"/><path d="M19 12h4"/><path d="M4.22 19.78l2.83-2.83"/><path d="M16.95 7.05l2.83-2.83"/>' },
projects: { paths: '<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>' },
quality: { paths: '<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>' },
assets: { paths: '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><line x1="12" y1="8" x2="12" y2="3"/>' },
subcontracting: { paths: '<circle cx="9" cy="7" r="4"/><circle cx="15" cy="7" r="4"/><path d="M3 21v-2a4 4 0 014-4h2"/><path d="M15 15h2a4 4 0 014 4v2"/>' },
organization: { paths: '<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="12" y1="3" x2="12" y2="21"/>' },
accounting: { paths: '<rect x="4" y="4" width="16" height="16" rx="2"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="8" y1="8" x2="8" y2="8.01" stroke-width="2.5"/><line x1="16" y1="16" x2="16" y2="16.01" stroke-width="2.5"/>' },
framework: { paths: '<polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"/><line x1="12" y1="22" x2="12" y2="15.5"/><polyline points="22 8.5 12 15.5 2 8.5"/>' },
"erpnext settings": { paths: '<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>' },
settings: { paths: '<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>' },
"accounts setup": { paths: '<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>' },
"account setup": { paths: '<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>' },
hr: { paths: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>' },
"hr module": { paths: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>' },
"frappe hr": { paths: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>' },
invoicing: { paths: '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>' },
payments: { paths: '<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>' },
"financial reports": { paths: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>' },
taxes: { paths: '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/>' },
banking: { paths: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01"/><path d="M10 8h.01"/><path d="M14 8h.01"/><line x1="2" y1="12" x2="22" y2="12"/>' },
budget: { paths: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>' },
"share management": { paths: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>' },
subscription: { paths: '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22 6 12 13 2 6"/>' },
crm: { paths: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>' },
support: { paths: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>' },
_fallback: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>'
}
};
/* ==================================================================
Helpers
================================================================== */
function currentTheme() {
return document.documentElement.getAttribute("data-jey-theme") || "chrome";
}
function getIconDef(key) {
var theme = currentTheme();
var icons = THEME_ICONS[theme] || THEME_ICONS.chrome;
var icon = key && icons[key];
if (icon) return icon;
// fallback to chrome if modern doesn't have the key
icon = key && THEME_ICONS.chrome[key];
if (icon) return icon;
return icons._fallback || THEME_ICONS.chrome._fallback;
}
/** Returns true only if key has an explicit entry (not _fallback). */
function hasIconDef(key) {
if (!key) return false;
var theme = currentTheme();
var icons = THEME_ICONS[theme] || THEME_ICONS.chrome;
if (icons[key] && key !== "_fallback") return true;
if (THEME_ICONS.chrome[key] && key !== "_fallback") return true;
return false;
}
function makeSvg(icon) {
var paths = typeof icon === "string" ? icon : icon.paths;
var cls = (typeof icon === "object" && icon.cls) ? icon.cls : "";
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("class", "jey-icon" + (cls ? " " + cls : ""));
svg.innerHTML = paths;
return svg;
}
function getIconKey(el) {
var desktop = el.closest(".desktop-icon");
if (!desktop) return null;
var id = desktop.getAttribute("data-id");
return id ? id.toLowerCase() : null;
}
function buildChrome() {
var c = document.createElement("div");
c.className = "jey-chrome";
c.innerHTML =
'<div class="jey-chrome-ring"><div class="jey-ring-glow"></div></div>' +
'<div class="jey-chrome-gap"></div>' +
'<div class="jey-chrome-ring-inner"></div>' +
'<div class="jey-chrome-face">' +
'<div class="jey-face-glow"></div>' +
'<div class="jey-face-reflection"></div>' +
"</div>";
return { chrome: c, face: c.querySelector(".jey-chrome-face") };
}
/* ==================================================================
Icon processing
================================================================== */
function wrapIcon(iconContainer) {
if (iconContainer.classList.contains("jey-chrome-done")) return;
iconContainer.classList.add("jey-chrome-done");
// Hide original children
Array.from(iconContainer.children).forEach(function (child) {
child.style.display = "none";
});
var parts = buildChrome();
var key = getIconKey(iconContainer);
parts.face.appendChild(makeSvg(getIconDef(key)));
iconContainer.appendChild(parts.chrome);
}
var DESKTOP_LABEL_OVERRIDES = {
"Frappe HR": "HR",
"ERPNext Settings": "Settings"
};
function renameDesktopLabels() {
Object.keys(DESKTOP_LABEL_OVERRIDES).forEach(function (id) {
var newLabel = DESKTOP_LABEL_OVERRIDES[id];
document.querySelectorAll('.desktop-icon[data-id="' + id + '"]').forEach(function (icon) {
var title = icon.querySelector(".icon-title");
if (title && title.textContent.trim() !== newLabel) {
title.textContent = newLabel;
title.setAttribute("data-original-title", newLabel);
}
});
});
}
function processIcons() {
document.querySelectorAll(
".desktop-icon > .icon-container:not(.jey-chrome-done)"
).forEach(function (c) {
if (c.closest(".folder-icon")) return;
wrapIcon(c);
});
document.querySelectorAll(
".desktop-icon > .icon-container.folder-icon:not(.jey-chrome-done)"
).forEach(wrapIcon);
renameDesktopLabels();
staggerAnimations();
}
function staggerAnimations() {
document.querySelectorAll(".jey-ring-glow").forEach(function (el, i) {
el.style.animationDelay = i * -0.6 + "s";
});
document.querySelectorAll(".jey-face-glow").forEach(function (el, i) {
el.style.animationDelay = i * -0.6 + "s";
});
document.querySelectorAll(".jey-face-reflection").forEach(function (el, i) {
el.style.animationDelay = i * -0.8 + "s";
});
}
/* ==================================================================
Context menu icon replacement
Replaces default ERPNext icons in .frappe-menu dropdowns with
the current theme's SVG icons (same as desktop).
================================================================== */
/* Menu titles are localized (e.g. Azerbaijani "Mühasibat uçotu"), so the
lookup key is taken from the untranslated img src filename first, then
from a reverse-lookup over frappe.boot.desktop_icons by __(label), and
only last from the displayed text as a best-effort fallback. */
function resolveEnglishKey(item) {
var img = item.querySelector("img.logo[src]");
if (img) {
var m = img.getAttribute("src").match(/\/([^\/]+)\.svg(\?|$)/);
if (m) return m[1].replace(/_/g, " ").toLowerCase();
}
var titleEl = item.querySelector(".menu-item-title, .header-title");
if (!titleEl) return null;
var titleText = titleEl.textContent.trim();
if (window.frappe && frappe.boot && Array.isArray(frappe.boot.desktop_icons) && typeof __ === "function") {
for (var i = 0; i < frappe.boot.desktop_icons.length; i++) {
var dIcon = frappe.boot.desktop_icons[i];
if (dIcon && dIcon.label && __(dIcon.label) === titleText) {
return dIcon.label.toLowerCase();
}
}
}
return titleText.toLowerCase();
}
function processContextMenuIcons() {
document.querySelectorAll(".frappe-menu .dropdown-menu-item").forEach(function (item) {
var iconDiv = item.querySelector(".menu-item-icon");
if (!iconDiv || iconDiv.querySelector(".jey-icon")) return;
// Skip chevron icons (the second .menu-item-icon with margin-left:auto)
if (iconDiv.style.marginLeft === "auto") return;
var key = resolveEnglishKey(item);
if (!hasIconDef(key)) return;
iconDiv.innerHTML = "";
iconDiv.appendChild(makeSvg(getIconDef(key)));
});
}
/* ==================================================================
Frappe-menu lucide icon remap
Some dropdowns (the user/settings menu at the top-left) render as
<svg><use href="#icon-NAME"> from the lucide sprite. Rewrite those
to semantically-close-but-different sprite ids so the theme feels
distinct from stock Frappe. Idempotent via .jey-menu-remap-done.
================================================================== */
var MENU_LUCIDE_ICON_MAP = {
"home": "house",
"sliders-horizontal": "settings-2",
"rotate-ccw": "refresh-ccw",
"maximize": "chevrons-left-right",
"moon": "moon-star",
"info": "life-buoy",
"logout": "log-out"
};
function processMenuLucideIcons() {
document.querySelectorAll(".frappe-menu .dropdown-menu-item").forEach(function (item) {
if (item.classList.contains("jey-menu-remap-done")) return;
item.classList.add("jey-menu-remap-done");
item.querySelectorAll(".menu-item-icon svg > use[href^='#icon-']").forEach(function (useEl) {
// Leave chevron-right (submenu indicator) alone.
var from = useEl.getAttribute("href").slice("#icon-".length);
if (from === "chevron-right") return;
var to = MENU_LUCIDE_ICON_MAP[from];
if (!to || to === from) return;
var newHref = "#icon-" + to;
useEl.setAttribute("href", newHref);
if (useEl.hasAttribute("xlink:href")) {
useEl.setAttribute("xlink:href", newHref);
}
});
});
}
/* ==================================================================
Sidebar header icon replacement
Replaces <img> / alphabet SVGs inside .sidebar-header with
the current theme's SVG icon.
================================================================== */
function processSidebarIcons() {
document.querySelectorAll(".sidebar-header").forEach(function (header) {
var logo = header.querySelector(".header-logo");
if (!logo || logo.querySelector(".jey-icon")) return;
var key = resolveEnglishKey(header);
if (!hasIconDef(key)) return;
logo.innerHTML = "";
logo.appendChild(makeSvg(getIconDef(key)));
});
}
/* ==================================================================
Workspace sidebar item icon remap
Rewrites <use href="#icon-NAME"> inside .sidebar-item-icon[item-icon]
to point at a different but semantically related sprite id.
Idempotent via .jey-side-item-done marker class.
================================================================== */
var SIDEBAR_ITEM_ICON_MAP = {
/* ERPNext / timeless legacy names */
"home": "house",
"accounting": "landmark",
"assets": "warehouse",
"buying": "shopping-cart",
"crm": "handshake",
"customer": "contact-round",
"customization": "palette",
"expenses": "banknote-arrow-up",
"getting-started": "rocket",
"hr": "users-round",
"integration": "plug-2",
"liabilities": "scale",
"loan": "piggy-bank",
"money-coins-1": "coins",
"non-profit": "heart-handshake",
"organization": "building-2",
"project": "folder-kanban",
"projects": "kanban",
"quality": "badge-check",
"sell": "trending-up",
"setting": "settings-2",
"stock": "package-2",
"support": "headphones",
"tool": "wrench",
"web": "earth",
"website": "globe",
"chart": "chart-column",
"criticize": "thumbs-down",
"review": "book-open-check",
"image-view": "image",
"up-arrow": "arrow-up",
"table_2": "table-properties",
"book-text": "library",
/* Lucide names found in workspace_sidebar fixtures */
"arrow-down-from-line": "download",
"arrow-left-to-line": "panel-left-close",
"arrow-right-from-line": "log-out",
"at-sign": "mail",
"badge-alert": "triangle-alert",
"badge-cent": "circle-dollar-sign",
"banknote-arrow-up": "trending-up",
"banknote-x": "receipt",
"bell": "bell-ring",
"bell-dot": "bell-ring",
"bell-ring": "megaphone",
"book-open-check": "clipboard-check",
"book-open-text": "book-marked",
"book-user": "contact-round",
"briefcase-business": "briefcase",
"building-2": "landmark",
"cable": "network",
"calendar-check": "calendar-check-2",
"calendar-check-2": "calendar-fold",
"calendar-clock": "timer",
"calendar-days": "calendar-range",
"calendar-heart": "heart",
"calendar-range": "calendar-fold",
"calendar-sync": "rotate-cw",
"caravan": "tent",
"circle-dollar-sign": "coins",
"circle-user-round": "user-round",
"clipboard-check": "clipboard-list",
"clipboard-pen": "notebook-pen",
"computer": "monitor",
"database": "hard-drive",
"database-backup": "save",
"dollar-sign": "coins",
"earth": "globe",
"factory": "warehouse",
"file": "file-text",
"file-lock": "shield",
"file-question-mark": "file-text",
"file-up": "upload",
"file-user": "contact-round",
"flag": "flame",
"folder-tree": "list-tree",
"funnel": "filter",
"git-pull-request-arrow": "git-branch",
"goal": "target",
"graduation-cap": "school",
"grid-2x2-check": "layout-grid",
"hammer": "wrench",
"heading": "text",
"heart-pulse": "stethoscope",
"import": "file-up",
"inspection-panel": "clipboard-check",
"laptop": "monitor",
"layout-dashboard": "gauge",
"lightbulb": "zap",
"link": "link-2",
"list": "list-checks",
"list-end": "list-ordered",
"list-todo": "clipboard-list",
"list-tree": "folder-tree",
"lock-keyhole": "shield",
"lock-keyhole-open": "shield-alert",
"logs": "scroll-text",
"mail": "mail-open",
"mail-x": "inbox",
"message-square-reply": "speech",
"milestone": "flag",
"monitor-check": "square-check",
"move-horizontal": "arrow-left-right",
"notebook-pen": "square-pen",
"notebook-text": "notepad-text",
"notepad-text": "scroll-text",
"orbit": "atom",
"panel-bottom-close": "panel-top-open",
"panel-top-close": "panel-bottom-open",
"pen-tool": "pen-line",
"person-standing": "user-round",
"piggy-bank": "vault",
"plane": "send",
"pointer": "mouse-pointer-2",
"printer": "file-output",
"receipt": "receipt-text",
"receipt-text": "scroll-text",
"repeat": "rotate-cw",
"repeat-2": "rotate-ccw",
"scroll-text": "book-text",
"server": "database",
"settings": "sliders-horizontal",
"sheet": "file-spreadsheet",
"split": "git-branch",
"sprout": "leaf",
"square-activity": "heart-pulse",
"square-pen": "pen-line",
"square-user-round": "contact-round",
"star": "sparkles",
"table": "table-2",
"thumbs-up": "heart",
"trash": "archive",
"trending-down": "chart-line",
"triangle": "triangle-alert",
"truck": "ship",
"tv-minimal": "monitor",
"upload": "cloud-upload",
"user": "user-round",
"user-cog": "user-round-cog",
"user-lock": "shield",
"user-round": "circle-user-round",
"user-round-check": "user-check",
"user-round-minus": "user-x",
"user-star": "award",
"users": "users-round",
"users-round": "contact-round",
"videotape": "film",
"wallpaper": "image",
"washing-machine": "container",
"webhook": "cable"
};
function processSidebarItemIcons() {
var nodes = document.querySelectorAll(
".body-sidebar .sidebar-item-icon[item-icon]:not(.jey-side-item-done)"
);
if (!nodes.length) return;
for (var i = 0; i < nodes.length; i++) {
var span = nodes[i];
var from = (span.getAttribute("item-icon") || "").trim().toLowerCase();
// Always mark done — even on unknown / identity — so we never revisit.
span.classList.add("jey-side-item-done");
var to = SIDEBAR_ITEM_ICON_MAP[from];
if (!to || to === from) continue;
var useEl = span.querySelector("svg > use");
if (!useEl) continue;
var newHref = "#icon-" + to;
if (useEl.getAttribute("href") !== newHref) {
useEl.setAttribute("href", newHref);
}
if (useEl.hasAttribute("xlink:href") &&
useEl.getAttribute("xlink:href") !== newHref) {
useEl.setAttribute("xlink:href", newHref);
}
span.setAttribute("data-jey-orig-icon", from);
span.setAttribute("item-icon", to);
}
}
/* ==================================================================
Rebuild all icons (called on theme switch)
================================================================== */
function rebuildAllIcons() {
document.querySelectorAll(".icon-container.jey-chrome-done").forEach(function (c) {
// Remove chrome DOM
var chrome = c.querySelector(".jey-chrome");
if (chrome) chrome.remove();
c.classList.remove("jey-chrome-done");
// Restore original children visibility (they stay hidden,
// wrapIcon will re-hide them anyway, but we need the class removed)
});
// Reset context menu / sidebar icons so they rebuild with new theme SVGs
document.querySelectorAll(".frappe-menu .menu-item-icon .jey-icon").forEach(function (svg) {
svg.remove();
});
document.querySelectorAll(".sidebar-header .header-logo .jey-icon").forEach(function (svg) {
svg.remove();
});
processIcons();
processContextMenuIcons();
processMenuLucideIcons();
processSidebarIcons();
processSidebarItemIcons();
}
// Expose for theme switcher
window.jeyRebuildIcons = rebuildAllIcons;
/* ==================================================================
Boot
================================================================== */
function processAll() { processIcons(); processContextMenuIcons(); processMenuLucideIcons(); processSidebarIcons(); processSidebarItemIcons(); }
$(document).on("page-change", processAll);
var attempts = 0;
var poll = setInterval(function () {
processAll();
attempts++;
if (attempts > 30) clearInterval(poll);
}, 300);
var observer = new MutationObserver(processAll);
observer.observe(document.body, { childList: true, subtree: true });
})();
/* ==========================================================================
Toggle Full Width button in sidebar (above Collapse)
========================================================================== */
(function () {
"use strict";
function addFullWidthButton() {
var collapse = document.querySelector(".collapse-sidebar-link");
if (!collapse || document.querySelector(".jey-fullwidth-link")) return;
var link = document.createElement("a");
link.className = "jey-fullwidth-link";
function updateIcon() {
link.innerHTML =
frappe.utils.icon("chevrons-left-right", "sm", "", "", "text-ink-gray-7 current-color", true) +
"<span>" + __("Full Width") + "</span>";
}
link.addEventListener("click", function () {
frappe.ui.toolbar.toggle_full_width();
updateIcon();
});
updateIcon();
collapse.parentNode.insertBefore(link, collapse);
}
$(document).ready(addFullWidthButton);
$(document).on("page-change", addFullWidthButton);
})();
/* ==========================================================================
Toggle Theme button in sidebar (above Collapse)
========================================================================== */
(function () {
"use strict";
function addThemeSwitchButton() {
var collapse = document.querySelector(".collapse-sidebar-link");
if (!collapse || document.querySelector(".jey-theme-switch-link")) return;
var link = document.createElement("a");
link.className = "jey-theme-switch-link";
link.innerHTML =
frappe.utils.icon("palette", "sm", "", "", "text-ink-gray-7 current-color", true) +
"<span>" + __("Switch Theme") + "</span>";
link.addEventListener("click", function () {
new frappe.ui.ThemeSwitcher().show();
});
collapse.parentNode.insertBefore(link, collapse);
}
$(document).ready(addThemeSwitchButton);
$(document).on("page-change", addThemeSwitchButton);
})();
/* ==========================================================================
Hide "Website" item from sidebar user dropdown
========================================================================== */
(function () {
"use strict";
function tagWebsiteItem() {
var label = __("Website");
document.querySelectorAll(".dropdown-menu-item:not(.jey-hidden-item)").forEach(function (el) {
var title = el.querySelector(".menu-item-title");
if (title && title.textContent.trim() === label) {
el.classList.add("jey-hidden-item");
}
});
}
$(document).ready(tagWebsiteItem);
var observer = new MutationObserver(tagWebsiteItem);
observer.observe(document.body, { childList: true, subtree: true });
})();
/* ==========================================================================
Reload button next to Notifications in sidebar
========================================================================== */
(function () {
"use strict";
function addReloadButton() {
var notif = document.querySelector(".sidebar-notification");
if (!notif || document.querySelector(".jey-reload-btn")) return;
var btn = notif.cloneNode(true);
btn.classList.remove("sidebar-notification");
btn.classList.remove("hidden");
btn.classList.add("jey-reload-btn");
btn.removeAttribute("id");
btn.setAttribute("title", __("Reload"));
var iconWrap = btn.querySelector(".sidebar-item-icon");
if (iconWrap) {
iconWrap.setAttribute("item-icon", "rotate-ccw");
iconWrap.innerHTML = frappe.utils.icon(
"rotate-ccw", "sm", "", "", "text-ink-gray-7 current-color", true
);
}
var label = btn.querySelector(".sidebar-item-label");
if (label) label.textContent = __("Reload");
btn.addEventListener("click", function (e) {
e.preventDefault();
e.stopPropagation();
frappe.ui.toolbar.clear_cache();
});
notif.parentNode.insertBefore(btn, notif.nextSibling);
}
$(document).ready(function () {
addReloadButton();
var tries = 0;
var poll = setInterval(function () {
addReloadButton();
if (++tries > 30 || document.querySelector(".jey-reload-btn")) {
clearInterval(poll);
}
}, 300);
});
$(document).on("page-change", addReloadButton);
})();
/* ==========================================================================
Fix: nested menus left orphaned when parent menu is toggled closed
(sidebar-header app-switcher → Workspaces submenu stays on screen)
========================================================================== */
(function () {
"use strict";
function patchMenuHide() {
if (!window.frappe || !frappe.ui || !frappe.ui.menu) return false;
var proto = frappe.ui.menu.prototype;
if (proto.__jeyHidePatched) return true;
var origHide = proto.hide;
proto.hide = function () {
if (this.nested_menus && this.nested_menus.length) {
this.nested_menus.forEach(function (m) { m && m.hide && m.hide(); });
}
this.current_menu = null;
return origHide.apply(this, arguments);
};
proto.__jeyHidePatched = true;
return true;
}
if (!patchMenuHide()) {
var tries = 0;
var poll = setInterval(function () {
if (patchMenuHide() || ++tries > 50) clearInterval(poll);
}, 200);
}
})();
/* ==========================================================================
Override Frappe Theme Switcher — Jey themes
========================================================================== */
(function () {
"use strict";
var JEY_THEMES = [
{
name: "chrome",
label: "Chrome",
info: "Metallic chrome buttons with animated rings",
},
{
name: "modern",
label: "Modern",
info: "Frosted glass with soft shadows",
},
];
function getCurrentJeyTheme() {
return document.documentElement.getAttribute("data-jey-theme") || "chrome";
}
function setJeyTheme(name) {
document.documentElement.setAttribute("data-jey-theme", name);
localStorage.setItem("jey-theme", name);
// Rebuild icons with the new theme's SVGs
if (window.jeyRebuildIcons) window.jeyRebuildIcons();
}
$(document).ready(function () {
if (!frappe || !frappe.ui) return;
frappe.ui.ThemeSwitcher = class JeyThemeSwitcher {
constructor() {
this.current = getCurrentJeyTheme();
this.setup_dialog();
}
setup_dialog() {
this.dialog = new frappe.ui.Dialog({
title: __("Switch Theme"),
});
this.body = $('<div class="jey-theme-grid"></div>').appendTo(
this.dialog.$body
);
this.render();
this.bind_keys();
}
bind_keys() {
this.dialog.$wrapper.on("keydown", (e) => {
var key = frappe.ui.keys.get_key(e);
if (key === "left" || key === "right") {
var idx = JEY_THEMES.findIndex((t) => t.name === this.current);
var next = key === "right" ? idx + 1 : idx - 1;
if (JEY_THEMES[next]) this.select(JEY_THEMES[next].name);
return false;
}
if (e.keyCode === 13) { this.dialog.hide(); return false; }
});
}
render() {
var me = this;
this.body.empty();
JEY_THEMES.forEach(function (theme) {
var selected = me.current === theme.name ? "selected" : "";
var $card = $([
'<div class="jey-theme-card ' + selected + '" data-theme="' + theme.name + '">',
' <div class="jey-theme-preview jey-theme-preview--' + theme.name + '">',
' <div class="jey-theme-preview-icon"></div>',
" </div>",
' <div class="jey-theme-label">' + __(theme.label) + "</div>",
' <div class="jey-theme-info">' + __(theme.info) + "</div>",
"</div>",
].join(""));
$card.on("click", function () { me.select(theme.name); });
me.body.append($card);
});
}
select(name) {
this.current = name;
setJeyTheme(name);
this.body.find(".jey-theme-card").removeClass("selected");
this.body.find('.jey-theme-card[data-theme="' + name + '"]').addClass("selected");
frappe.show_alert(__("Theme: " + name), 3);
}
show() {
this.current = getCurrentJeyTheme();
this.render();
this.dialog.show();
}
hide() {
this.dialog.hide();
}
};
});
})();
/* ==========================================================================
Grid Column Resize — drag handles on table header columns
Uses a persistent <style> tag so widths survive Frappe DOM rebuilds.
========================================================================== */
(function () {
"use strict";
var MIN_COL_WIDTH = 40;
var STORAGE_PREFIX = "jey-col-widths:";
// Reuse the <style> element created in the init IIFE
var styleEl = document.getElementById("jey-col-resize-styles");
// Runtime widths: { "Sales Invoice:items": { item_code: 200, ... }, ... }
var allWidths = {};
function rebuildStylesheet() {
var rules = [];
Object.keys(allWidths).forEach(function (gridKey) {
var widths = allWidths[gridKey];
Object.keys(widths).forEach(function (fieldname) {
var w = widths[fieldname];
rules.push(
'[data-jey-grid="' + gridKey + '"] .grid-static-col[data-fieldname="' + fieldname + '"]' +
" { flex: 0 0 " + w + "px !important; width: " + w + "px !important; max-width: none !important; }"
);
});
});
styleEl.textContent = rules.join("\n");
}
function getGridKey(grid) {
if (grid.dataset.jeyGrid) return grid.dataset.jeyGrid;
// Build stable key: "ParentDoctype:table_fieldname"
var control = grid.closest(".frappe-control[data-fieldname]");
var tableField = control ? control.getAttribute("data-fieldname") : null;
var doctype = cur_frm ? cur_frm.doctype : null;
var key = (doctype && tableField) ? doctype + ":" + tableField : null;
if (key) {
grid.dataset.jeyGrid = key;
return key;
}
return null;
}
function saveWidths(gridKey) {
if (!gridKey || !allWidths[gridKey]) return;
localStorage.setItem(STORAGE_PREFIX + gridKey, JSON.stringify(allWidths[gridKey]));
}
function loadWidths(gridKey) {
if (!gridKey) return null;
var stored = localStorage.getItem(STORAGE_PREFIX + gridKey);
if (stored) {
try { return JSON.parse(stored); } catch (e) { return null; }
}
return null;
}
function addHandlesToGrid(grid) {
var cols = grid.querySelectorAll(
".grid-heading-row .grid-row:not(.filter-row) .grid-static-col"
);
if (!cols.length) return;
var gridKey = getGridKey(grid);
if (!gridKey) return;
// Load saved widths from localStorage on first encounter
if (!allWidths[gridKey]) {
var saved = loadWidths(gridKey);
if (saved) {
allWidths[gridKey] = saved;
rebuildStylesheet();
}
}
cols.forEach(function (col) {
if (col.querySelector(".jey-col-resize")) return;
// Don't override position:sticky on sticky columns
if (!col.classList.contains("sticky-grid-col") && !col.classList.contains("row-index")) {
col.style.position = "relative";
}
var handle = document.createElement("div");
handle.className = "jey-col-resize";
col.appendChild(handle);
handle.addEventListener("mousedown", function (e) {
e.preventDefault();
e.stopPropagation();
startResize(grid, col, e.clientX);
});
handle.addEventListener("dblclick", function (e) {
e.preventDefault();
e.stopPropagation();
autoFitColumn(grid, col);
});
});
}
function autoFitColumn(grid, headerCol) {
var fieldname = headerCol.getAttribute("data-fieldname");
var gridKey = getGridKey(grid);
if (!fieldname || !gridKey) return;
// Measure header text width
var label = headerCol.querySelector(".static-area, .sort-header, span");
var text = label ? label.textContent.trim() : fieldname;
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var style = getComputedStyle(headerCol);
ctx.font = style.fontWeight + " " + style.fontSize + " " + style.fontFamily;
var textWidth = ctx.measureText(text).width;
// Add padding (8px each side + 16px buffer for sort icon etc)
var newWidth = Math.max(MIN_COL_WIDTH, Math.ceil(textWidth) + 32);
if (!allWidths[gridKey]) allWidths[gridKey] = {};
allWidths[gridKey][fieldname] = newWidth;
rebuildStylesheet();
saveWidths(gridKey);
}
function startResize(grid, headerCol, startX) {
var fieldname = headerCol.getAttribute("data-fieldname");
if (!fieldname) return;
var gridKey = getGridKey(grid);
if (!gridKey) return;
var startWidth = headerCol.getBoundingClientRect().width;
if (!allWidths[gridKey]) allWidths[gridKey] = {};
var widths = allWidths[gridKey];
// Lock all columns that aren't already saved
grid.querySelectorAll(
".grid-heading-row .grid-row:not(.filter-row) .grid-static-col"
).forEach(function (c) {
var fn = c.getAttribute("data-fieldname");
if (fn && !widths[fn]) {
widths[fn] = c.getBoundingClientRect().width;
}
});
rebuildStylesheet();
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
function onMove(e) {
var newWidth = Math.max(MIN_COL_WIDTH, startWidth + (e.clientX - startX));
widths[fieldname] = newWidth;
rebuildStylesheet();
}
function onUp() {
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
saveWidths(gridKey);
}
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
}
function processGrids() {
document.querySelectorAll(".form-grid").forEach(addHandlesToGrid);
}
// Add handles when grids appear — only needs to run for new header columns
var handleTimer = null;
function scheduleHandles() {
clearTimeout(handleTimer);
handleTimer = setTimeout(processGrids, 100);
}
$(document).on("page-change", scheduleHandles);
var observer = new MutationObserver(scheduleHandles);
observer.observe(document.body, { childList: true, subtree: true });
/* ==================================================================
Column Drag & Drop Reorder
Uses Frappe's native user_settings.GridView — order is applied
by Frappe BEFORE first render, so no flash.
================================================================== */
function getGridInstance(gridEl) {
var control = gridEl.closest(".frappe-control[data-fieldname]");
if (!control || typeof cur_frm === "undefined" || !cur_frm) return null;
var fieldname = control.getAttribute("data-fieldname");
var field = cur_frm.fields_dict[fieldname];
return field && field.grid ? field.grid : null;
}
function saveColumnOrder(gridInstance, newOrder) {
if (!gridInstance.frm) return;
var settings = newOrder.map(function (fieldname) {
var vc = gridInstance.visible_columns.find(function (v) {
return v[0].fieldname === fieldname;
});
if (!vc) return null;
return {
fieldname: fieldname,
columns: vc[1],
sticky: vc[0].sticky ? 1 : 0
};
}).filter(Boolean);
var parentDoctype = gridInstance.frm.doctype;
var childDoctype = gridInstance.doctype;
// Update in-memory cache for immediate re-render
if (!frappe.model.user_settings[parentDoctype]) {
frappe.model.user_settings[parentDoctype] = {};
}
if (!frappe.model.user_settings[parentDoctype].GridView) {
frappe.model.user_settings[parentDoctype].GridView = {};
}
frappe.model.user_settings[parentDoctype].GridView[childDoctype] = settings;
// Save to server directly (bypass comparison that would skip due to cache already updated)
frappe.model.user_settings.update(parentDoctype, frappe.model.user_settings[parentDoctype]);
}
// Custom mouse-based drag (more reliable than HTML5 drag API)
var dragState = null;
function addDragListeners(gridEl) {
var cols = gridEl.querySelectorAll(
".grid-heading-row .grid-row:not(.filter-row) .grid-static-col[data-fieldname]"
);
cols.forEach(function (col) {
if (col.dataset.jeyDraggable) return;
col.dataset.jeyDraggable = "1";
col.addEventListener("mousedown", function (e) {
// Don't start drag from resize handle
if (e.target.classList.contains("jey-col-resize")) return;
// Only left button
if (e.button !== 0) return;
var fieldname = col.getAttribute("data-fieldname");
if (!fieldname) return;
dragState = {
gridEl: gridEl,
fromField: fieldname,
startX: e.clientX,
startY: e.clientY,
dragging: false
};
});
});
}
// Global mouse handlers for drag
document.addEventListener("mousemove", function (e) {
if (!dragState) return;
// Start dragging after 5px movement (prevents accidental drag on click)
if (!dragState.dragging) {
var dx = Math.abs(e.clientX - dragState.startX);
var dy = Math.abs(e.clientY - dragState.startY);
if (dx < 5 && dy < 5) return;
dragState.dragging = true;
document.body.style.cursor = "grabbing";
document.body.style.userSelect = "none";
}
// Find which header column the mouse is over
var gridEl = dragState.gridEl;
gridEl.querySelectorAll(".jey-drop-target").forEach(function (c) {
c.classList.remove("jey-drop-target");
});
var target = document.elementFromPoint(e.clientX, e.clientY);
if (target) {
var col = target.closest(".grid-heading-row .grid-static-col[data-fieldname]");
if (col && col.getAttribute("data-fieldname") !== dragState.fromField) {
// Determine direction for indicator
var headerCols = Array.from(gridEl.querySelectorAll(
".grid-heading-row .grid-row:not(.filter-row) .grid-static-col[data-fieldname]"
));
var fi = headerCols.findIndex(function (c) { return c.getAttribute("data-fieldname") === dragState.fromField; });
var ti = headerCols.indexOf(col);
col.classList.add("jey-drop-target");
col.classList.toggle("jey-drop-right", fi < ti);
col.classList.toggle("jey-drop-left", fi >= ti);
dragState.toField = col.getAttribute("data-fieldname");
} else {
dragState.toField = null;
}
}
});
document.addEventListener("mouseup", function () {
if (!dragState) return;
var state = dragState;
dragState = null;
document.body.style.cursor = "";
document.body.style.userSelect = "";
state.gridEl.querySelectorAll(".jey-drop-target").forEach(function (c) {
c.classList.remove("jey-drop-target");
});
if (!state.dragging || !state.toField) return;
var gridInstance = getGridInstance(state.gridEl);
if (!gridInstance) return;
// Build new order
var headerCols = state.gridEl.querySelectorAll(
".grid-heading-row .grid-row:not(.filter-row) .grid-static-col[data-fieldname]"
);
var order = [];
headerCols.forEach(function (c) { order.push(c.getAttribute("data-fieldname")); });
var fromIdx = order.indexOf(state.fromField);
var toIdx = order.indexOf(state.toField);
if (fromIdx === -1 || toIdx === -1 || fromIdx === toIdx) return;
var draggingRight = fromIdx < toIdx;
// Remove dragged column
order.splice(fromIdx, 1);
// Find target in modified array
var newToIdx = order.indexOf(state.toField);
if (newToIdx === -1) return;
// Dragging right → insert AFTER target, left → insert BEFORE
order.splice(draggingRight ? newToIdx + 1 : newToIdx, 0, state.fromField);
saveColumnOrder(gridInstance, order);
gridInstance.reset_grid();
});
function processDragGrids() {
document.querySelectorAll(".form-grid").forEach(addDragListeners);
}
var dragTimer = null;
function scheduleDrag() {
clearTimeout(dragTimer);
dragTimer = setTimeout(processDragGrids, 150);
}
$(document).on("page-change", scheduleDrag);
var dragObserver = new MutationObserver(scheduleDrag);
dragObserver.observe(document.body, { childList: true, subtree: true });
})();
/* ==========================================================================
List View — Override SVG sprite icons
Replaces <symbol> content in Frappe's SVG sprite sheets so every
<use href="#icon-..."> picks up the new artwork automatically.
========================================================================== */
(function () {
"use strict";
var SPRITE_OVERRIDES = {
/* icon-list (Lucide, 24×24, stroke) — 2×2 grid of squares */
"icon-list":
'<rect x="3" y="3" width="8" height="8" rx="1.5"/>' +
'<rect x="13" y="3" width="8" height="8" rx="1.5"/>' +
'<rect x="3" y="13" width="8" height="8" rx="1.5"/>' +
'<rect x="13" y="13" width="8" height="8" rx="1.5"/>',
/* icon-sort-descending (Timeless, 16×16, stroke) — vertical bar chart tall→short */
"icon-sort-descending":
'<path d="M3 13V3M6.33 13V5.5M9.67 13V8M13 13V10.5" ' +
'stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>',
/* icon-sort-ascending (Timeless, 16×16, stroke) — vertical bar chart short→tall */
"icon-sort-ascending":
'<path d="M3 13V10.5M6.33 13V8M9.67 13V5.5M13 13V3" ' +
'stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>',
/* es-line-filter (Espresso, 16×16, fill) — solid funnel shape */
"es-line-filter":
'<g class="es-line-filter">' +
'<path d="M1.5 2.5h13L10 8v3.5H6V8L1.5 2.5z"/>' +
'</g>',
/* icon-home — simple house silhouette */
"icon-home":
'<path d="M12 1L0 12h4v10h7v-7h2v7h7V12h4z"/>'
};
var totalIcons = Object.keys(SPRITE_OVERRIDES).length;
function overrideSprites() {
var replaced = 0;
for (var id in SPRITE_OVERRIDES) {
var symbol = document.getElementById(id);
if (symbol && symbol.tagName.toLowerCase() === "symbol") {
symbol.innerHTML = SPRITE_OVERRIDES[id];
replaced++;
}
}
return replaced;
}
function tryOverride() {
if (overrideSprites() >= totalIcons) return;
var attempts = 0;
var timer = setInterval(function () {
if (overrideSprites() >= totalIcons || ++attempts > 20) {
clearInterval(timer);
}
}, 200);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", tryOverride);
} else {
tryOverride();
}
})();
/* ==========================================================================
Rebrand: ERPNext → Jey ERP, Frappe → Framework (visible text only)
========================================================================== */
(function () {
"use strict";
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1, IFRAME: 1, CANVAS: 1 };
/* Case-aware replacements: tries to preserve the surrounding case style */
function rebrandText(str) {
// ERPNext variants
str = str.replace(/ERPNext/g, "Jey ERP");
str = str.replace(/erpnext/gi, function (m) {
if (m === "ERPNEXT") return "JEY ERP";
return "Jey ERP";
});
// Frappe variants (but not inside URLs or identifiers)
str = str.replace(/Frapp[eé]/g, "Jey ERP");
str = str.replace(/frapp[eé]/gi, function (m) {
if (m.toUpperCase() === m) return "JEY ERP";
return "Jey ERP";
});
return str;
}
function needsRebrand(str) {
return /erpnext|frapp[eé]/i.test(str);
}
/* Walk text nodes inside an element */
function walkTextNodes(root) {
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
var node;
while ((node = walker.nextNode())) {
if (SKIP_TAGS[node.parentNode.tagName]) continue;
var text = node.nodeValue;
if (needsRebrand(text)) {
node.nodeValue = rebrandText(text);
}
}
}
/* Rebrand visible attributes: placeholder, title, alt */
function walkAttributes(root) {
var els = root.querySelectorAll("[placeholder], [title], [alt]");
for (var i = 0; i < els.length; i++) {
var el = els[i];
["placeholder", "title", "alt"].forEach(function (attr) {
var val = el.getAttribute(attr);
if (val && needsRebrand(val)) {
el.setAttribute(attr, rebrandText(val));
}
});
}
}
/* Rebrand the page title */
function rebrandTitle() {
if (needsRebrand(document.title)) {
document.title = rebrandText(document.title);
}
}
function processNode(root) {
walkTextNodes(root);
walkAttributes(root);
rebrandTitle();
}
/* Initial pass */
function init() {
processNode(document.body);
rebrandTitle();
/* Watch for dynamically added content */
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
for (var j = 0; j < m.addedNodes.length; j++) {
var node = m.addedNodes[j];
if (node.nodeType === 1 && !SKIP_TAGS[node.tagName]) {
processNode(node);
} else if (node.nodeType === 3 && node.parentNode && !SKIP_TAGS[node.parentNode.tagName] && needsRebrand(node.nodeValue)) {
node.nodeValue = rebrandText(node.nodeValue);
}
}
/* Handle direct text changes (e.g. frappe sets .textContent) */
if (m.type === "characterData" && m.target.nodeType === 3) {
if (m.target.parentNode && !SKIP_TAGS[m.target.parentNode.tagName] && needsRebrand(m.target.nodeValue)) {
m.target.nodeValue = rebrandText(m.target.nodeValue);
}
}
}
rebrandTitle();
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
/* ==========================================================================
Form Tabs — Re-trigger section cascade on tab switch
========================================================================== */
(function () {
document.addEventListener("click", function (e) {
var tab = e.target.closest(".form-tabs .nav-link");
if (!tab) return;
// Small delay so Frappe shows the tab content first
setTimeout(function () {
var sections = document.querySelectorAll(".form-layout .form-section .section-head, .form-layout .form-section .section-body");
sections.forEach(function (s) {
s.style.animation = "none";
// Force reflow then restart animation
void s.offsetHeight;
s.style.animation = "";
});
}, 20);
});
})();
/* ==========================================================================
Form Sections — Set exact max-height for smooth collapse animation
========================================================================== */
(function () {
document.addEventListener("click", function (e) {
var head = e.target.closest(".section-head.collapsible");
if (!head) return;
var body = head.nextElementSibling;
if (!body || !body.classList.contains("section-body")) return;
// Set exact max-height so transition has a real start/end value
body.style.maxHeight = body.scrollHeight + "px";
// Force reflow — browser must register this value before Frappe toggles .collapsed
void body.offsetHeight;
}, true);
})();
/* ==========================================================================
Search Modal — smooth close (backdrop cleanup)
========================================================================== */
// Bootstrap leaves the backdrop when we override display.
// Remove it manually when .show is removed.
(function () {
var searchModal = null;
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
var m = mutations[i];
if (m.attributeName === "class" && searchModal) {
if (!searchModal.classList.contains("show") && searchModal.dataset.jeyWasOpen) {
delete searchModal.dataset.jeyWasOpen;
// After transition ends, clean up backdrop
setTimeout(function () {
var bd = document.querySelector(".modal-backdrop");
if (bd) bd.remove();
document.body.classList.remove("modal-open");
document.body.style.removeProperty("padding-right");
}, 550);
}
if (searchModal.classList.contains("show")) {
searchModal.dataset.jeyWasOpen = "1";
// Re-focus input after CSS transition so awesomplete opens
var input = searchModal.querySelector("#navbar-search");
if (input) {
setTimeout(function () {
input.blur();
input.focus();
}, 550);
}
}
}
}
});
// Find search modal and start observing
var bodyObserver = new MutationObserver(function () {
if (searchModal) return;
var el = document.querySelector(".modal:has(.cool-awesomebar-modal-footer)");
if (!el) return;
searchModal = el;
bodyObserver.disconnect();
observer.observe(searchModal, { attributes: true, attributeFilter: ["class"] });
// If .show is already present (race condition on first open), trigger focus now
if (searchModal.classList.contains("show")) {
searchModal.dataset.jeyWasOpen = "1";
var input = searchModal.querySelector("#navbar-search");
if (input) {
setTimeout(function () {
input.blur();
input.focus();
}, 550);
}
}
});
bodyObserver.observe(document.body, { childList: true, subtree: true });
})();