From b44294d4c864b2837ec8ac39c3b25f4de57bbac8 Mon Sep 17 00:00:00 2001 From: Ahsan Javaid Date: Mon, 31 Aug 2026 14:48:51 +0200 Subject: [PATCH] v2.1.3: dark-mode sidebar contrast, slide-in row editor, workspace card polish Dark-mode active sidebar item now a solid pill (WCAG AA verified across brand colors) instead of a barely-visible 15%-opacity tint. Child table rows open in a right-side slide-in panel instead of expanding inline, with a Theme Studio toggle to fall back to the classic behavior. Fixed Workspace Shortcut/Links cards looking flat and washed out, and stray card styling that was wrapping plain text section labels (e.g. "SHORTCUTS") in their own thin white/dark card. --- CHANGELOG.md | 10 ++ README.md | 3 + solvronix_desk/__init__.py | 2 +- solvronix_desk/hooks.py | 8 +- solvronix_desk/public/css/dark_mode.css | 3 +- .../public/css/grid_row_slide_panel.css | 95 ++++++++++++ solvronix_desk/public/css/polish.css | 5 +- solvronix_desk/public/css/solvronix_desk.css | 94 +++++++++++- .../public/js/grid_row_slide_panel.js | 86 +++++++++++ .../page/theme_studio/theme_studio.js | 1 + solvronix_desk/theme_engine.py | 7 +- tests/grid_row_slide_panel.test.js | 139 ++++++++++++++++++ tests/test_workspace_editorjs_popover_css.py | 2 +- 13 files changed, 440 insertions(+), 15 deletions(-) create mode 100644 solvronix_desk/public/css/grid_row_slide_panel.css create mode 100644 solvronix_desk/public/js/grid_row_slide_panel.js create mode 100644 tests/grid_row_slide_panel.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 47749e6..0d74fc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [2.1.3] — 2026-08-31 + +### Added +- Child table rows (e.g. Sales Invoice Items) now open in a right-side slide-in panel instead of expanding inline in the table — full row height, header stays pinned with row actions (Duplicate, Move, Insert Above/Below, Delete) always visible, footer stays pinned with shortcut hints. Drops in over a dimming backdrop; closes via the backdrop, Escape, or Frappe's own header/footer controls. Frappe's native row behavior (row switching, Ctrl+Up/Down navigation, save-on-close) is untouched — the panel is CSS-only, animated off Frappe's own `display:block/none` toggle on `.form-in-grid` via `@starting-style`/`transition-behavior: allow-discrete`, since `GridRow`/`GridRowForm` aren't exposed globally for patching. New Theme Studio toggle (Layout → "Slide-in child table row editor", on by default) falls back to the standard inline expansion when off + +### Fixed +- In dark mode, the active sidebar item was nearly invisible — only a 15%-opacity accent tint over the already brand-colored dark sidebar background, relying almost entirely on a thin 3px left border to show selection. Now a solid pill matching light mode's treatment, darkened via relative-color lightness scaling (hue/saturation preserved) rather than mixing toward black — the latter washes out saturation badly on light/pale custom accents (e.g. a pale yellow lost ~38 points of saturation, reading as muddy grey-brown instead of gold). Verified against WCAG AA (4.5:1, worst case 7.2:1) across five accent colors — default, near-black, pure-saturated, desaturated, and pale — using the same contrast formula Theme Studio's own accessibility audit uses +- Workspace Shortcut/Links cards (e.g. a workspace's "Shortcuts"/"Meta" sections) looked flat and washed out on every workspace — a near-invisible `rgba(0,0,0,0.06)` border and a subtle idle shadow left cards barely distinguishable from the page canvas, and count badges fell back to a flat neutral gray whenever a shortcut had no configured color. Cards now use the theme's real card-border token with a stronger idle shadow, and gain a brand-accent top stripe on hover (matching the App Grid's existing card language); default-color count badges pick up an accent tint instead of flat gray — scoped specifically to Shortcut/Links widgets so ordinary status pills elsewhere keep their normal semantic gray meaning +- Section labels typed as plain bold text in a workspace's content editor (e.g. Frappe CRM's "SHORTCUTS"/"META" headings) were being wrapped in their own stray white/dark card — a thin, oddly-shaped sliver rather than a real card. Root cause: Frappe's workspace editor tags every content block, including plain text paragraphs, with a bare `.widget` class meant only for spacing; the theme's card rules in `solvronix_desk.css`, `polish.css`, and the runtime `theme_engine.py` output were all painting card background/border/shadow onto that class unconditionally, and even after excluding EditorJS content blocks from all three, Frappe core's own un-important `.widget` rule was still shining through underneath with a plain background/radius. Added an explicit reset for EditorJS content blocks (`.widget.cdx-block`) so plain text renders as text, while real functional widgets (Shortcut, Links, Chart, Number Card, etc. — which don't carry that class) keep their normal card look. Workspaces that use Frappe's newer structured "Shortcuts"/"Links" group title (`.widget-group-title`) also get a proper shaded header bar instead of a bare label, matching the list-view column header treatment already used elsewhere in the theme + ## [2.1.2] — 2026-08-11 ### Fixed diff --git a/README.md b/README.md index 45bd27d..9bcdcfa 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,9 @@ You set one brand color. The system automatically generates your complete color ### Icon Rail Sidebar A slim, always-visible app-icon column sits beside the classic workspace list — one compact tile per installed app (ERPNext, CRM, Frappe HR, and so on), each using that app's own icon in a theme-colored tile that follows your brand/accent color automatically. New apps and workspaces appear on the rail automatically. Collapses to icon-only via its own toggle, independent of the classic sidebar. Switch back to the classic single-column Tree layout at any time from Theme Studio → Navbar & Sidebar → Sidebar Layout. +### Slide-In Row Editor +Child table rows (Sales Invoice Items, and any other grid) open in a full-height panel that slides in from the right instead of expanding inline in the table — header stays pinned with row actions (Duplicate, Move, Insert Above/Below, Delete), footer stays pinned with keyboard shortcut hints. Closes via the dimming backdrop, `Esc`, or Frappe's own controls; row switching and `Ctrl+↑`/`Ctrl+↓` navigation work exactly as before. Toggle it off in Theme Studio → Layout to fall back to the classic inline row editor. + ### Dark Mode — Light / Dark / Auto One click cycles between light, dark, and auto. Auto follows your operating system theme and switches live when the OS does. Both modes respect your brand colors. Works on every page. diff --git a/solvronix_desk/__init__.py b/solvronix_desk/__init__.py index 4eabd0b..e835b9d 100644 --- a/solvronix_desk/__init__.py +++ b/solvronix_desk/__init__.py @@ -1 +1 @@ -__version__ = "2.1.2" +__version__ = "2.1.3" diff --git a/solvronix_desk/hooks.py b/solvronix_desk/hooks.py index a942bdb..3931282 100644 --- a/solvronix_desk/hooks.py +++ b/solvronix_desk/hooks.py @@ -7,7 +7,7 @@ app_license = "MIT" app_color = "#E8610A" app_icon = "octicon octicon-paintcan" -app_version = "2.1.2" +app_version = "2.1.3" required_apps = [] @@ -18,17 +18,18 @@ # ── AUTHENTICATED DESK ASSETS ────────────────────────────────────────────────── # Query versions are bumped whenever an asset changes to invalidate browser cache. app_include_css = [ - "/assets/solvronix_desk/css/solvronix_desk.css?v=54", + "/assets/solvronix_desk/css/solvronix_desk.css?v=58", "/assets/solvronix_desk/css/sidebar.css?v=31", "/assets/solvronix_desk/css/command_palette.css?v=4", "/assets/solvronix_desk/css/smart_home.css?v=7", "/assets/solvronix_desk/css/progressive_forms.css?v=3", "/assets/solvronix_desk/css/notification_center.css?v=3", - "/assets/solvronix_desk/css/polish.css?v=3", + "/assets/solvronix_desk/css/polish.css?v=4", "/assets/solvronix_desk/css/dark_mode.css?v=15", "/assets/solvronix_desk/css/module_cards.css?v=2", "/assets/solvronix_desk/css/density.css?v=2", "/assets/solvronix_desk/css/theme_studio.css?v=19", + "/assets/solvronix_desk/css/grid_row_slide_panel.css?v=1", ] app_include_js = [ "/assets/solvronix_desk/js/dark_mode.js?v=12", @@ -41,6 +42,7 @@ "/assets/solvronix_desk/js/module_cards.js?v=10", "/assets/solvronix_desk/js/theme_runtime.js?v=8", "/assets/solvronix_desk/js/chart_runtime.js?v=4", + "/assets/solvronix_desk/js/grid_row_slide_panel.js?v=1", ] # ── BOOT / INSTALL / DOCUMENT LIFECYCLE HOOKS ────────────────────────────────── diff --git a/solvronix_desk/public/css/dark_mode.css b/solvronix_desk/public/css/dark_mode.css index f6525bc..01a2f42 100644 --- a/solvronix_desk/public/css/dark_mode.css +++ b/solvronix_desk/public/css/dark_mode.css @@ -267,7 +267,8 @@ html[data-theme="dark"] .datatable .dt-cell { html[data-theme="dark"] .datatable .dt-cell--header, html[data-theme="dark"] .list-row-head, -html[data-theme="dark"] .dt-header { +html[data-theme="dark"] .dt-header, +html[data-theme="dark"] .widget-group-head { background-color: var(--st-input-bg) !important; border-color: rgba(255,255,255,0.08) !important; } diff --git a/solvronix_desk/public/css/grid_row_slide_panel.css b/solvronix_desk/public/css/grid_row_slide_panel.css new file mode 100644 index 0000000..a614e59 --- /dev/null +++ b/solvronix_desk/public/css/grid_row_slide_panel.css @@ -0,0 +1,95 @@ +/* ============================================================================= + Solvronix Desk — Slide-in child table row editor (Task 3) + Frappe 16 already opens a grid row's edit form inline in the table + (GridRowForm's .form-in-grid, toggled via plain display:block/none — there + is no modal to intercept, and GridRow/GridRowForm aren't exposed as + frappe.ui.form.GridRow anywhere, so they can't be monkey-patched from here). + Rather than relocate the DOM node or patch inaccessible classes, this lets + Frappe's own display toggle drive a native slide-in/out animation via + @starting-style + transition-behavior: allow-discrete — CSS handles both + open and close directions, so all of Frappe's own state (row toggling, + keyboard nav, switching rows) keeps working exactly as built. + Gated behind body.st-grid-slide-panel-enabled, set by + grid_row_slide_panel.js from the Theme Studio toggle — absent, this file + has no effect and Frappe's default inline behavior is untouched. + ============================================================================= */ + +body.st-grid-slide-panel-enabled .form-in-grid { + position: fixed !important; + top: var(--st-header-height, 0) !important; + right: 0 !important; + left: auto !important; + bottom: 0 !important; + width: min(480px, 100vw) !important; + height: auto !important; + max-width: 100vw; + margin: 0 !important; + background: var(--st-card-bg) !important; + border: none !important; + border-left: 1px solid var(--st-border) !important; + box-shadow: -8px 0 24px rgba(0, 0, 0, 0.18) !important; + z-index: 1050; + overflow-y: auto; + transform: translateX(0); + transition: + transform var(--st-transition), + display var(--st-transition) allow-discrete, + overlay var(--st-transition) allow-discrete; +} + +@starting-style { + body.st-grid-slide-panel-enabled .form-in-grid { + transform: translateX(100%); + } +} + +/* Header/footer stay pinned, body scrolls — same structure Frappe already + builds (.grid-form-heading / .grid-form-body / .grid-footer-toolbar), + just re-laid-out as a proper panel instead of an inline block. */ +body.st-grid-slide-panel-enabled .form-in-grid .grid-form-heading { + position: sticky; + top: 0; + z-index: 2; + background: var(--st-card-bg); + border-bottom: 1px solid var(--st-border); + padding: 12px 16px; +} +body.st-grid-slide-panel-enabled .form-in-grid .grid-form-body { + display: flex; + flex-direction: column; + min-height: calc(100% - 48px); +} +body.st-grid-slide-panel-enabled .form-in-grid .form-area { + flex: 1 1 auto; + padding: 4px 16px; +} +body.st-grid-slide-panel-enabled .form-in-grid .grid-footer-toolbar { + position: sticky; + bottom: 0; + background: var(--st-card-bg); + border-top: 1px solid var(--st-border); + padding: 10px 16px; + margin-top: auto; +} + +/* Backdrop — a single shared element toggled by grid_row_slide_panel.js + whenever any .form-in-grid panel is open; click-to-close is wired there. */ +.st-grid-slide-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.35); + z-index: 1040; + opacity: 0; + pointer-events: none; + transition: opacity var(--st-transition); +} +.st-grid-slide-backdrop.st-open { + opacity: 1; + pointer-events: auto; +} + +@media (max-width: 640px) { + body.st-grid-slide-panel-enabled .form-in-grid { + width: 100vw !important; + } +} diff --git a/solvronix_desk/public/css/polish.css b/solvronix_desk/public/css/polish.css index ab21706..6146a3f 100644 --- a/solvronix_desk/public/css/polish.css +++ b/solvronix_desk/public/css/polish.css @@ -77,7 +77,10 @@ a { } /* ── 3. CARDS — border + layered shadow + hover lift ───────────── */ -.widget, +/* :not(.cdx-block) — Frappe's workspace editor tags every content block, + including plain text paragraphs, with a bare .widget class; without this + exclusion a one-line text label gets card border/shadow treatment too. */ +.widget:not(.cdx-block), .frappe-card { border: 1px solid var(--st-card-border); box-shadow: var(--st-shadow-sm); diff --git a/solvronix_desk/public/css/solvronix_desk.css b/solvronix_desk/public/css/solvronix_desk.css index 29c0438..b2d1aa5 100644 --- a/solvronix_desk/public/css/solvronix_desk.css +++ b/solvronix_desk/public/css/solvronix_desk.css @@ -154,7 +154,17 @@ textarea, --st-text-muted: #94A3B8; --st-bg: var(--st-page-bg); /* backwards-compat alias */ /* Sidebar vars restored to dark-on-brand for dark mode */ - --st-sidebar-active: color-mix(in srgb, var(--st-accent) 15%, transparent); + /* Solid pill, matching light mode's treatment (var(--st-sidebar-active): var(--st-accent)) + — a 15%-opacity tint over the already brand-colored dark sidebar was nearly invisible, + leaving only the 3px left border to show which item is active. + Darkened via relative-color lightness scaling (hue/saturation untouched) rather than + color-mix() toward black — mixing toward black in sRGB compresses saturation hardest on + colors that start light (e.g. a pale custom accent loses ~38 points of saturation), which + reads as muddy rather than a clean darker shade of the same color. + WCAG-verified with the runtime's auto-picked --st-sidebar-active-text: stays >=4.5:1 (worst + case 7.2:1) across default accent, near-black, pure-saturated, desaturated, and pale/light + custom accents (same ratio formula as theme_studio.js's _ratio()/_contrast()). */ + --st-sidebar-active: hsl(from var(--st-accent) h s calc(l * 0.55)); --st-sidebar-hover: rgba(255, 255, 255, 0.10); --st-sidebar-text: #FFFFFF; --st-sidebar-text-muted: rgba(255, 255, 255, 0.55); @@ -702,20 +712,42 @@ input[type="checkbox"]:checked { /* ── WORKSPACE / HOME CARDS ─────────────────────── */ -/* Cards — layered shadow, no top stripe */ +/* Frappe's workspace editor tags every content block (plain text paragraphs, + headers, lists — not just real ShortcutWidget/ChartWidget instances) with a + bare .widget class for spacing. Frappe core's own .widget rule (no + !important) unconditionally paints background-color/border-radius onto + that class, so a one-line text label like a bold "SHORTCUTS" heading was + rendering as its own stray white/dark card. Excluding .cdx-block from the + theme's OWN card rules (below, and in polish.css/theme_engine.py) only + stops the theme from reinforcing that — core's un-important rule still + shows through underneath, so it needs an explicit reset here too. */ +.widget.cdx-block { + background: transparent !important; + border-radius: 0 !important; + padding: 4px 0 !important; +} + +/* Cards — stronger separation from the page canvas than a bare shadow-md gave; + border now uses the theme's real card-border token instead of a near-invisible + flat rgba, so cards read as distinct surfaces at rest, not just on hover. */ .desk-card, .widget.onboarding-widget-box, .widget.links-widget-box, .widget.shortcut-widget-box { + position: relative; border-top: none !important; border-radius: var(--st-radius-lg) !important; - box-shadow: var(--st-shadow-md) !important; - border: 1px solid rgba(0,0,0,0.06) !important; + box-shadow: 0 2px 8px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.08) !important; + border: 1px solid var(--st-card-border) !important; transition: all var(--st-transition) !important; overflow: hidden; } .desk-card:hover, -.widget:hover { +/* :not(.cdx-block) — Frappe's workspace editor tags EVERY content block (plain + text paragraphs, not just real ShortcutWidget/ChartWidget instances) with a + bare .widget class for spacing; without this exclusion a one-line text + label like a bold "SHORTCUTS" heading gets the same hover lift as a card. */ +.widget:hover:not(.cdx-block) { box-shadow: var(--st-shadow-lg) !important; transform: translateY(-3px); border-color: var(--st-primary-light) !important; @@ -724,6 +756,58 @@ input[type="checkbox"]:checked { color: var(--st-accent) !important; } +/* Accent top stripe on hover — scoped to Shortcut/Links widgets only (not the + broad .widget selector) so Number Card/Chart widgets are untouched; mirrors + the App Grid's .st-ws-card::before treatment for one consistent card language. */ +.widget.shortcut-widget-box::before, +.widget.links-widget-box::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--st-accent); + opacity: 0; + transition: opacity var(--st-transition); +} +.widget.shortcut-widget-box:hover::before, +.widget.links-widget-box:hover::before { + opacity: 1; +} + +/* Section group labels (SHORTCUTS / META / PORTAL, etc.) — a real header bar + (background fill, not just a hairline border) using the same shaded-bg + convention already used for list-view/datatable column headers, so the + label reads as a distinct section header instead of a barely-visible line. */ +.widget-group-head { + background: color-mix(in srgb, var(--st-page-bg) 92%, #000) !important; + border: 1px solid var(--st-border) !important; + border-radius: var(--st-radius-sm) !important; + padding: 9px 14px !important; + margin-bottom: 14px !important; +} +.widget-group-head .widget-group-title { + color: var(--st-text-muted) !important; + font-size: 12px !important; + font-weight: 700 !important; + letter-spacing: 0.08em !important; + text-transform: uppercase !important; + margin-bottom: 0 !important; +} + +/* Shortcut/Links widget count badge — accent tint instead of the flat neutral + gray Frappe falls back to when a workspace shortcut has no configured color. + Scoped to these two widget types so ordinary status pills elsewhere (list + views, kanban, etc.) keep their normal semantic gray meaning untouched. */ +.widget.shortcut-widget-box .indicator-pill.gray, +.widget.shortcut-widget-box .indicator-pill.grey, +.widget.links-widget-box .indicator-pill.gray, +.widget.links-widget-box .indicator-pill.grey { + background: color-mix(in srgb, var(--st-accent) 12%, transparent) !important; + color: var(--st-accent) !important; +} + /* ── PAGE HEAD ───────────────────────────────────────────────────────────────── */ /* Light mode: clean white card with accent left stripe */ html:not([data-theme="dark"]) .desk-page > .page-head, diff --git a/solvronix_desk/public/js/grid_row_slide_panel.js b/solvronix_desk/public/js/grid_row_slide_panel.js new file mode 100644 index 0000000..c0c9c12 --- /dev/null +++ b/solvronix_desk/public/js/grid_row_slide_panel.js @@ -0,0 +1,86 @@ +/* ================================================================ + Solvronix Desk — Slide-in child table row editor (Task 3) + The actual open/close animation is pure CSS (grid_row_slide_panel.css, + @starting-style + transition-behavior: allow-discrete driven by Frappe's + own display:block/none toggle on .form-in-grid) — Frappe's GridRow/ + GridRowForm classes aren't exposed globally, so patching them isn't an + option, and this approach needs none of that: it doesn't touch Frappe's + own open/close logic at all, just restyles what's already there. + This file only does two small, additive things neither Frappe nor CSS + alone can: (1) gate the feature behind the Theme Studio toggle, and + (2) show/hide a shared backdrop, since a slide-in panel with no dimming + behind it doesn't read as "modal-like" at a glance. + ================================================================ */ +(function () { + "use strict"; + + function isEnabled() { + var cfg = (window.frappe && frappe.boot && frappe.boot.st_theme_config) || {}; + return cfg.grid_row_slide_panel !== false; // default on, explicit false opts out + } + + if (!isEnabled()) return; + document.documentElement.classList.add("st-grid-slide-panel-enabled"); + document.body ? + document.body.classList.add("st-grid-slide-panel-enabled") : + document.addEventListener("DOMContentLoaded", function () { + document.body.classList.add("st-grid-slide-panel-enabled"); + }); + + var $backdrop = null; + + function ensureBackdrop() { + if ($backdrop) return $backdrop; + $backdrop = $('
').appendTo("body"); + $backdrop.on("click", function () { + // Close via Frappe's own toggle — the currently-open panel's header/footer + // already has Frappe's real click-to-close handler wired (it travels with + // the element regardless of how it's styled), so this triggers the same + // row.toggle_view() Frappe's own UI would. + var $open = $(".form-in-grid:visible").first(); + $open.find(".grid-form-heading").trigger("click"); + }); + return $backdrop; + } + + function syncBackdrop() { + // Check the inline style Frappe actually set, not jQuery's :visible — + // the CSS uses transition-behavior: allow-discrete so the panel keeps + // rendering (and reads as :visible) for the whole slide-out animation; + // el.style.display reflects Frappe's real intent immediately instead. + var anyOpen = false; + document.querySelectorAll(".form-in-grid").forEach(function (el) { + if (el.style.display !== "none") anyOpen = true; + }); + ensureBackdrop().toggleClass("st-open", anyOpen); + } + + // Frappe only ever toggles display:block/none on an already-in-DOM + // .form-in-grid (created once per row, reused after) — a MutationObserver + // on style/class attribute changes, scoped to grid containers, is the + // established pattern this codebase already uses elsewhere (see + // solvronix_desk.js's sidebar-repatch observer) for reacting to state + // Frappe manages internally without an exposed hook. + var observer = new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + var target = mutations[i].target; + if (target.classList && target.classList.contains("form-in-grid")) { + syncBackdrop(); + return; + } + } + }); + + observer.observe(document.body, { + attributes: true, + attributeFilter: ["style"], + subtree: true, + }); + + // Escape closes the open panel the same way the backdrop click does. + document.addEventListener("keydown", function (e) { + if (e.key !== "Escape") return; + var $open = $(".form-in-grid:visible").first(); + if ($open.length) $open.find(".grid-form-heading").trigger("click"); + }); +})(); diff --git a/solvronix_desk/solvronix_desk/page/theme_studio/theme_studio.js b/solvronix_desk/solvronix_desk/page/theme_studio/theme_studio.js index be1478e..4dc8b25 100644 --- a/solvronix_desk/solvronix_desk/page/theme_studio/theme_studio.js +++ b/solvronix_desk/solvronix_desk/page/theme_studio/theme_studio.js @@ -123,6 +123,7 @@ solvronix_desk.theme_studio_sections = [ ["header_height", "Toolbar height", "range", 32, 64, "px"], ["sticky_navbar", "Sticky top toolbar", "check"], ["sticky_form_toolbar", "Sticky form toolbar", "check"], ["compact_forms", "Compact forms", "check"], + ["grid_row_slide_panel", "Slide-in child table row editor", "check"], ], }, { diff --git a/solvronix_desk/theme_engine.py b/solvronix_desk/theme_engine.py index 47797ae..722b86a 100644 --- a/solvronix_desk/theme_engine.py +++ b/solvronix_desk/theme_engine.py @@ -137,6 +137,7 @@ "sticky_navbar": True, "sticky_form_toolbar": True, "compact_forms": False, + "grid_row_slide_panel": True, # Accessibility "high_contrast": False, "large_text": False, @@ -178,7 +179,7 @@ "sidebar_auto_collapse", "sticky_navbar", "sticky_form_toolbar", "compact_forms", "hide_powered", "high_contrast", "large_text", "enforce_wcag", "enable_custom_js", "enable_command_palette", - "enable_smart_home", + "enable_smart_home", "grid_row_slide_panel", } INT_RANGES = { "sidebar_width": (200, 360), @@ -1178,7 +1179,7 @@ def render_css(config, enabled=True): .dropdown-menu,.frappe-menu,.awesomplete > ul {{ background: var(--st-dropdown-bg) !important; }} .form-control[readonly],.control-value.like-disabled-input {{ background: var(--st-readonly-bg) !important; }} .form-control:disabled,.btn:disabled,[aria-disabled="true"] {{ opacity: var(--st-disabled-opacity) !important; }} -.frappe-card,.form-page,.widget,.desk-card,.number-card {{ +.frappe-card,.form-page,.widget:not(.cdx-block),.desk-card,.number-card {{ background-color: var(--st-card-bg) !important; border-color: var(--st-card-border) !important; border-radius: var(--st-card-radius) !important; box-shadow: var(--st-shadow-md) !important; }} @@ -1190,7 +1191,7 @@ def render_css(config, enabled=True): background-color: var(--st-table-header) !important; border-color: var(--st-report-grid) !important; }} .dt-cell,.grid-row .data-row .row,.report-view .datatable {{ border-color: var(--st-report-grid) !important; }} -.workspace .widget,.shortcut-widget-box,.links-widget-box {{ background: var(--st-workspace-card) !important; }} +.workspace .widget:not(.cdx-block),.shortcut-widget-box,.links-widget-box {{ background: var(--st-workspace-card) !important; }} .editor-js-container .ce-header, .editor-js-container .ce-header .h4, .editor-js-container .ce-header b {{ diff --git a/tests/grid_row_slide_panel.test.js b/tests/grid_row_slide_panel.test.js new file mode 100644 index 0000000..4575514 --- /dev/null +++ b/tests/grid_row_slide_panel.test.js @@ -0,0 +1,139 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); +const vm = require("node:vm"); + +const modulePath = path.join( + __dirname, "..", "solvronix_desk", "public", "js", "grid_row_slide_panel.js" +); + +function fakeJQueryElement(overrides = {}) { + const classes = new Set(); + const handlers = {}; + const el = { + classes, + appendTo() { return el; }, + on(event, handler) { handlers[event] = handler; return el; }, + trigger(event) { handlers[event] && handlers[event](); return el; }, + toggleClass(name, force) { + const add = force === undefined ? !classes.has(name) : force; + add ? classes.add(name) : classes.delete(name); + return el; + }, + hasClass(name) { return classes.has(name); }, + find() { return fakeJQueryElement(); }, + first() { return el; }, + length: 1, + ...overrides, + }; + return el; +} + +function loadModule({ config = {}, formInGridEls = [] } = {}) { + const bodyClasses = new Set(); + const docElClasses = new Set(); + let observerCallback = null; + const keydownHandlers = []; + + class MutationObserver { + constructor(callback) { observerCallback = callback; } + observe() {} + disconnect() {} + } + + const backdrop = fakeJQueryElement(); + const $ = (selectorOrHtml) => { + if (typeof selectorOrHtml === "string" && selectorOrHtml.includes("st-grid-slide-backdrop")) { + return backdrop; + } + if (selectorOrHtml === ".form-in-grid:visible") { + const visible = formInGridEls.filter((e) => e.style.display !== "none"); + return { + length: visible.length, + first() { + const target = visible[0]; + return { + length: target ? 1 : 0, + find() { + return { trigger: () => target && target.onHeadingClick && target.onHeadingClick() }; + }, + }; + }, + }; + } + return fakeJQueryElement(); + }; + + const document = { + documentElement: { classList: { add(name) { docElClasses.add(name); } } }, + body: { classList: { add(name) { bodyClasses.add(name); } } }, + querySelectorAll(selector) { + if (selector === ".form-in-grid") return formInGridEls; + return []; + }, + addEventListener(type, handler) { if (type === "keydown") keydownHandlers.push(handler); }, + }; + + const context = { + console, + document, + window: { frappe: { boot: { st_theme_config: config } } }, + frappe: { boot: { st_theme_config: config } }, + MutationObserver, + $, + }; + vm.createContext(context); + vm.runInContext(fs.readFileSync(modulePath, "utf8"), context, { filename: modulePath }); + + return { + bodyClasses, + docElClasses, + backdrop, + fireMutation(target) { observerCallback([{ target }]); }, + fireEscape() { keydownHandlers.forEach((h) => h({ key: "Escape" })); }, + }; +} + +test("feature is on by default and gates via the Theme Studio config flag", () => { + const enabled = loadModule({ config: {} }); + assert.equal(enabled.bodyClasses.has("st-grid-slide-panel-enabled"), true); + assert.equal(enabled.docElClasses.has("st-grid-slide-panel-enabled"), true); + + const disabled = loadModule({ config: { grid_row_slide_panel: false } }); + assert.equal(disabled.bodyClasses.has("st-grid-slide-panel-enabled"), false); +}); + +test("backdrop opens/closes off Frappe's real inline style, not layout-based visibility", () => { + const panel = { classList: { contains: () => true }, style: { display: "block" } }; + const runtime = loadModule({ config: {}, formInGridEls: [panel] }); + + runtime.fireMutation(panel); + assert.equal(runtime.backdrop.hasClass("st-open"), true); + + // Frappe's allow-discrete CSS transition keeps the element rendering (and + // jQuery's :visible true) for the whole close animation -- only the + // inline style Frappe actually set reflects its real intent immediately, + // which is what the observer must react to instead. + panel.style.display = "none"; + runtime.fireMutation(panel); + assert.equal(runtime.backdrop.hasClass("st-open"), false); +}); + +test("backdrop click and Escape close the currently open panel via Frappe's own toggle", () => { + let headingClicked = false; + const panel = { + classList: { contains: () => true }, + style: { display: "block" }, + onHeadingClick() { headingClicked = true; }, + }; + const runtime = loadModule({ config: {}, formInGridEls: [panel] }); + + runtime.fireMutation(panel); + runtime.backdrop.trigger("click"); + assert.equal(headingClicked, true); + + headingClicked = false; + runtime.fireEscape(); + assert.equal(headingClicked, true); +}); diff --git a/tests/test_workspace_editorjs_popover_css.py b/tests/test_workspace_editorjs_popover_css.py index d38e838..032e64b 100644 --- a/tests/test_workspace_editorjs_popover_css.py +++ b/tests/test_workspace_editorjs_popover_css.py @@ -39,7 +39,7 @@ def test_block_with_open_editorjs_popover_is_promoted_before_focus(self): def test_workspace_css_cache_is_bumped(self): hooks = HOOKS.read_text(encoding="utf-8") - self.assertIn("/assets/solvronix_desk/css/solvronix_desk.css?v=54", hooks) + self.assertIn("/assets/solvronix_desk/css/solvronix_desk.css?v=58", hooks) self.assertIn("/assets/solvronix_desk/js/solvronix_desk.js?v=63", hooks)