1294 lines
56 KiB
JavaScript
1294 lines
56 KiB
JavaScript
/* ==========================================================================
|
||
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 & 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 & 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)));
|
||
});
|
||
}
|
||
|
||
/* ==================================================================
|
||
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();
|
||
processSidebarIcons();
|
||
processSidebarItemIcons();
|
||
}
|
||
|
||
// Expose for theme switcher
|
||
window.jeyRebuildIcons = rebuildAllIcons;
|
||
|
||
/* ==================================================================
|
||
Boot
|
||
================================================================== */
|
||
|
||
function processAll() { processIcons(); processContextMenuIcons(); 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() {
|
||
var on = JSON.parse(localStorage.getItem("container_fullwidth") || "false");
|
||
var iconName = on ? "minimize" : "maximize";
|
||
link.innerHTML =
|
||
frappe.utils.icon(iconName, "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);
|
||
})();
|
||
|
||
/* ==========================================================================
|
||
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 && !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 (!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();
|
||
}
|
||
})();
|