feat(silk): match chrome's coverage + fix chart animation & tooltip
Coverage parity — silk now styles the components chrome did but silk didn't (ported into silk's glass/champagne idiom, not chrome's metal): - child-table grids (flattened cells, glass focus well, read-only fill) - alerts/toasts, awesomplete suggestion panel, danger button, form sidebar - timeline connector + dot, list-view sticky right column, print reset - misc: popover body, navbar links, breadcrumb container Chart tooltip: was dark bg with silk's dark --charts-tooltip-* text vars (dark-on-dark, unreadable) — now an ivory panel matching the dropdowns. Chart entrance animation: frappe-charts mounts an empty flat skeleton, then re-renders with real data async, replacing the line path — the old draw class re-fired on each new path (flat line -> real -> flicker -> redraw), and the skeleton axis morph (1..5 -> 100K) showed through. Now two-phase (drawChart in jey_theme.js): .jey-chart-prep hides the whole chart until data settles, then one .jey-chart-draw reveals it with a single left-to-right clip-path wipe so line + area + dots draw on together. Verified headless: single wipe, no morph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dc36d3d46a
commit
8fcd2a5999
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -972,6 +972,9 @@
|
|||
localStorage.setItem("jey-theme", name);
|
||||
// Rebuild icons with the new theme's SVGs
|
||||
if (window.jeyRebuildIcons) window.jeyRebuildIcons();
|
||||
// Silk's dashboard motion (count-up, chart draw) is gated on the
|
||||
// current theme; re-run it so switching INTO silk animates live.
|
||||
if (window.jeyDashboardMotion) window.jeyDashboardMotion(true);
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
|
@ -1049,6 +1052,137 @@
|
|||
});
|
||||
})();
|
||||
|
||||
/* ==========================================================================
|
||||
Workspace card icon tiles
|
||||
The silk mockup gives every shortcut a leading icon tile and every KPI card
|
||||
a champagne bubble; core renders neither. The rendered DOM can't tell us
|
||||
what a card points at either — data-widget-name is a row hash and the label
|
||||
is translated — so hook frappe.widget.make_widget, which does see the real
|
||||
options (link_to on a shortcut, document_type on the Number Card doc).
|
||||
|
||||
The tiles are inert markup under any other theme: only theme_silk gives
|
||||
.jey-sc-tile / .jey-kpi-tile a `display`, so chrome never shows them and no
|
||||
rebuild is needed on theme switch.
|
||||
========================================================================== */
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/* Lucide-style paths, lifted from the mockup so the shapes match it. */
|
||||
var TILE_ICONS = {
|
||||
user: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||||
truck: '<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.7a1 1 0 0 0-.2-.6l-3.5-4.3a1 1 0 0 0-.8-.4H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/>',
|
||||
box: '<path d="M21 8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="M3.3 7 12 12l8.7-5"/><path d="M12 22V12"/>',
|
||||
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M16 13H8"/><path d="M16 17H8"/>',
|
||||
cart: '<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/>',
|
||||
wallet: '<rect width="20" height="14" x="2" y="5" rx="2"/><path d="M2 10h20"/><path d="M6 15h4"/>',
|
||||
chart: '<path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="m19 9-5 5-4-4-3 3"/>',
|
||||
tag: '<path d="M12.6 2.6A2 2 0 0 0 11.2 2H4a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l8.7 8.7a2.4 2.4 0 0 0 3.4 0l6.6-6.6a2.4 2.4 0 0 0 0-3.4z"/><circle cx="7.5" cy="7.5" r="1.2"/>',
|
||||
calendar: '<rect width="18" height="18" x="3" y="4" rx="2"/><path d="M16 2v4"/><path d="M8 2v4"/><path d="M3 10h18"/>',
|
||||
wrench: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.8-3.8a6 6 0 0 1-7.9 7.9l-6.9 6.9a2.1 2.1 0 0 1-3-3l6.9-6.9a6 6 0 0 1 7.9-7.9l-3.8 3.8z"/>',
|
||||
page: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
||||
link: '<path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.8 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/>'
|
||||
};
|
||||
|
||||
/* Matched against the untranslated DocType/Report name, so these English
|
||||
keywords hold regardless of the UI language. First match wins — order
|
||||
matters (Supplier must beat the person rule, Sales Order must beat the
|
||||
invoice rule). */
|
||||
var TILE_RULES = [
|
||||
[/report|analytic|ledger|statement|trend|dashboard/, "chart"],
|
||||
[/supplier|vendor|delivery|shipment|freight/, "truck"],
|
||||
[/customer|lead|contact|employee|user|shareholder|sales person|salesperson|applicant/, "user"],
|
||||
[/invoice|receipt|voucher|bill|slip|note/, "file"],
|
||||
[/order|quotation|request|opportunity|demand/, "cart"],
|
||||
[/payment|journal|account|expense|payroll|salary|bank|budget|currency/, "wallet"],
|
||||
[/item|stock|warehouse|product|batch|serial|bom|material|bin|asset|packing/, "box"],
|
||||
[/price|tax|discount|coupon|promotion|pricing|terms/, "tag"],
|
||||
[/shift|attendance|leave|holiday|timesheet|schedule|calendar|appointment/, "calendar"],
|
||||
[/setting|setup|config|tool|template|rule|type|group|category/, "wrench"]
|
||||
];
|
||||
|
||||
function tilePaths(name, type) {
|
||||
if (type === "URL") return TILE_ICONS.link;
|
||||
var s = String(name || "").toLowerCase();
|
||||
for (var i = 0; i < TILE_RULES.length; i++) {
|
||||
if (TILE_RULES[i][0].test(s)) return TILE_ICONS[TILE_RULES[i][1]];
|
||||
}
|
||||
if (type === "Report" || type === "Dashboard") return TILE_ICONS.chart;
|
||||
if (type === "Page") return TILE_ICONS.page;
|
||||
return TILE_ICONS.file;
|
||||
}
|
||||
|
||||
function makeTile(cls, paths) {
|
||||
var span = document.createElement("span");
|
||||
span.className = cls;
|
||||
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.innerHTML = paths;
|
||||
span.appendChild(svg);
|
||||
return span;
|
||||
}
|
||||
|
||||
function decorate(widget, opts) {
|
||||
var el = widget && widget.widget && widget.widget[0];
|
||||
if (!el) return;
|
||||
|
||||
if (opts.widget_type === "shortcut") {
|
||||
if (el.querySelector(".jey-sc-tile")) return;
|
||||
var head = el.querySelector(".widget-head");
|
||||
if (!head) return;
|
||||
// First child of the flex head -> [tile][label ...][control]
|
||||
head.insertBefore(
|
||||
makeTile("jey-sc-tile", tilePaths(widget.link_to || widget.label, widget.type)),
|
||||
head.firstChild
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.widget_type === "number_card") {
|
||||
// The card doc arrives async; ask for it ourselves rather than
|
||||
// racing NumberCardWidget.make_card(). with_doc is cached, so this
|
||||
// is the same fetch, not an extra round trip.
|
||||
frappe.model
|
||||
.with_doc("Number Card", opts.number_card_name || opts.name)
|
||||
.then(function (card) {
|
||||
if (!card || !el.isConnected || el.querySelector(".jey-kpi-tile")) return;
|
||||
el.appendChild(
|
||||
makeTile("jey-kpi-tile", tilePaths(card.document_type || card.report_name, "DocType"))
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function patch() {
|
||||
if (!window.frappe || !frappe.widget || !frappe.widget.make_widget) return false;
|
||||
if (frappe.widget.make_widget.__jey) return true;
|
||||
|
||||
var orig = frappe.widget.make_widget;
|
||||
var wrapped = function (opts) {
|
||||
var w = orig.apply(this, arguments);
|
||||
try {
|
||||
decorate(w, opts || {});
|
||||
} catch (e) {
|
||||
// A broken tile must never take the workspace down with it.
|
||||
console.warn("jey_theme: widget tile failed", e);
|
||||
}
|
||||
return w;
|
||||
};
|
||||
wrapped.__jey = true;
|
||||
frappe.widget.make_widget = wrapped;
|
||||
return true;
|
||||
}
|
||||
|
||||
// jey_theme.js can parse before frappe's desk bundle has defined
|
||||
// frappe.widget, so keep trying until it shows up.
|
||||
if (!patch()) {
|
||||
var tries = 0;
|
||||
var timer = setInterval(function () {
|
||||
if (patch() || ++tries > 300) clearInterval(timer);
|
||||
}, 50);
|
||||
}
|
||||
})();
|
||||
|
||||
/* ==========================================================================
|
||||
Grid Column Resize — drag handles on table header columns
|
||||
Uses a persistent <style> tag so widths survive Frappe DOM rebuilds.
|
||||
|
|
@ -2608,3 +2742,127 @@
|
|||
if (window.$ && window.frappe) $(document).on("app_ready", loadEditor);
|
||||
if (window.frappe && frappe.boot) loadEditor();
|
||||
})();
|
||||
|
||||
/* ==========================================================================
|
||||
Silk dashboard motion — count-up & chart line-draw
|
||||
Ported from the silk mockup's MOTION SYSTEM (silk_dashboard_crisp.html):
|
||||
number cards roll from 0 to their value on arrival, and the cash-flow line
|
||||
draws itself on. Both are SILK-ONLY (gated on data-jey-theme) and skipped
|
||||
under prefers-reduced-motion. The rest of the mockup's motion (arrival
|
||||
cascade, hover/press) is pure CSS in theme_silk.
|
||||
|
||||
Number values and chart SVGs render async and get replaced on every widget
|
||||
refresh, so a debounced MutationObserver drives the scan; each element is
|
||||
marked once (__jeyRolled / __jeyDrawn) so re-scans are cheap and idempotent.
|
||||
========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var REDUCED = window.matchMedia && matchMedia("(prefers-reduced-motion:reduce)").matches;
|
||||
function isSilk() {
|
||||
return document.documentElement.getAttribute("data-jey-theme") === "silk";
|
||||
}
|
||||
|
||||
/* ---- count-up: roll each figure from 0 to its real value ----
|
||||
Preserves the surrounding text (₹, %, "L"/"Cr", nbsp) and decimals. */
|
||||
var NUM = /\d[\d,]*\.?\d*/; /* first number token */
|
||||
function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
|
||||
|
||||
function roll(el) {
|
||||
if (el.__jeyRolled) return;
|
||||
var tpl = el.textContent, m = tpl.match(NUM);
|
||||
if (!m) return;
|
||||
var raw = m[0], clean = raw.replace(/,/g, ""), target = parseFloat(clean);
|
||||
if (!isFinite(target)) return;
|
||||
el.__jeyRolled = true;
|
||||
var dot = clean.indexOf("."), decimals = dot > -1 ? clean.length - dot - 1 : 0;
|
||||
var comma = raw.indexOf(",") > -1;
|
||||
var pre = tpl.slice(0, m.index), post = tpl.slice(m.index + raw.length);
|
||||
function fmt(v) {
|
||||
var s = v.toFixed(decimals);
|
||||
return comma
|
||||
? Number(s).toLocaleString("en-IN", { minimumFractionDigits: decimals, maximumFractionDigits: decimals })
|
||||
: s;
|
||||
}
|
||||
el.textContent = pre + fmt(0) + post;
|
||||
var dur = 900, t0 = null;
|
||||
function step(ts) {
|
||||
if (t0 === null) t0 = ts;
|
||||
var p = Math.min(1, (ts - t0) / dur);
|
||||
el.textContent = pre + fmt(target * easeOut(p)) + post;
|
||||
if (p < 1) requestAnimationFrame(step);
|
||||
else el.textContent = tpl; /* restore exact original markup */
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
/* ---- chart line-draw: reveal the line ONCE, after it settles ----
|
||||
frappe-charts mounts an empty flat-baseline skeleton, then re-renders with
|
||||
real data when the async fetch lands, REPLACING the line path (often more
|
||||
than once). The old code added .jey-chart-draw on first sight, so the
|
||||
stroke-reveal keyframe re-fired on every replaced path — the user saw the
|
||||
flat line, the real line, then a flicker + a second draw.
|
||||
|
||||
Now it's two-phase: hide everything with .jey-chart-prep up front (so the
|
||||
skeleton and every intermediate render stay invisible), debounce until the
|
||||
chart stops mutating, then swap to .jey-chart-draw for a single reveal and
|
||||
drop the class once it has played so later re-renders show a static line.
|
||||
A hard deadline guarantees the reveal even if the page never goes quiet —
|
||||
the marks are hidden via opacity, so a stuck prep must never be permanent. */
|
||||
function revealChart(svg) {
|
||||
if (svg.__jeyDrawnDone) return;
|
||||
if (!svg.querySelector(".line-graph-path")) return;
|
||||
svg.__jeyDrawnDone = true;
|
||||
if (svg.__jeySettle) { clearTimeout(svg.__jeySettle); svg.__jeySettle = null; }
|
||||
if (svg.__jeyHard) { clearTimeout(svg.__jeyHard); svg.__jeyHard = null; }
|
||||
svg.classList.remove("jey-chart-prep");
|
||||
svg.classList.add("jey-chart-draw");
|
||||
/* after the wipe (svg fade 0.35s, draw-area wipe 0.12+1.05s) fully plays,
|
||||
drop the class so a later re-render shows the chart statically. */
|
||||
setTimeout(function () { svg.classList.remove("jey-chart-draw"); }, 1800);
|
||||
}
|
||||
|
||||
function drawChart(svg) {
|
||||
if (svg.__jeyDrawnDone) return;
|
||||
if (!svg.querySelector(".line-graph-path")) return; /* line charts only */
|
||||
if (!svg.classList.contains("jey-chart-draw")) svg.classList.add("jey-chart-prep");
|
||||
if (!svg.__jeyHard) svg.__jeyHard = setTimeout(function () { revealChart(svg); }, 1800);
|
||||
if (svg.__jeySettle) clearTimeout(svg.__jeySettle);
|
||||
svg.__jeySettle = setTimeout(function () { revealChart(svg); }, 380);
|
||||
}
|
||||
|
||||
function scan(force) {
|
||||
if (!isSilk() || REDUCED) return;
|
||||
if (force) {
|
||||
document.querySelectorAll(".number-widget-box .widget-content .number").forEach(function (el) {
|
||||
el.__jeyRolled = false;
|
||||
});
|
||||
document.querySelectorAll(".widget svg.frappe-chart").forEach(function (svg) {
|
||||
svg.__jeyDrawnDone = false;
|
||||
if (svg.__jeySettle) { clearTimeout(svg.__jeySettle); svg.__jeySettle = null; }
|
||||
if (svg.__jeyHard) { clearTimeout(svg.__jeyHard); svg.__jeyHard = null; }
|
||||
svg.classList.remove("jey-chart-draw");
|
||||
svg.classList.remove("jey-chart-prep");
|
||||
});
|
||||
}
|
||||
document.querySelectorAll(".number-widget-box .widget-content .number").forEach(roll);
|
||||
document.querySelectorAll(".widget svg.frappe-chart").forEach(drawChart);
|
||||
}
|
||||
|
||||
window.jeyDashboardMotion = scan;
|
||||
|
||||
var pending = null;
|
||||
function schedule() {
|
||||
if (pending) return;
|
||||
pending = setTimeout(function () { pending = null; scan(false); }, 150);
|
||||
}
|
||||
|
||||
if (document.readyState !== "loading") schedule();
|
||||
else document.addEventListener("DOMContentLoaded", schedule);
|
||||
|
||||
/* Workspaces mount and re-render their widgets long after DOMContentLoaded;
|
||||
a debounced observer catches each render without polling. */
|
||||
if (window.MutationObserver) {
|
||||
new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in New Issue