grids: polish child-table active/read-only cells; misc chrome polish

- Chrome theme: active grid row now visually matches the static row —
  flattened inputs, forced bottom border, padding match, pressed-in
  button-style effect on focused cells with smooth 0.45s transition.
- Read-only grid cells get a distinct lighter-grey bg only while their
  row is the active one (uses Frappe's display:none on .field-area).
- Button press animations: expanded shadow set + explicit transitions
  so press/release is symmetric and eased.
- Shared: fixed section-in keyframe leaving a stacking context that
  clipped Link autocomplete dropdowns; dropdown fade-out without
  Popper position reset; smooth sidebar expand to match collapse.
- JS: clip section overflow during collapse transition, restore after
  transitionend; freeze dropdown position during Bootstrap hide.
- CLAUDE.md: rewritten as Claude Code guidance doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-04-17 17:08:50 +00:00
parent 5cbc36566b
commit 953bf8b2c8
4 changed files with 412 additions and 76 deletions

107
CLAUDE.md
View File

@ -1,57 +1,84 @@
# Jey Theme
# CLAUDE.md
Frappe theme override app. Custom UI/UX for ERPNext desk.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this app is
Frappe/ERPNext v15 app that does three things:
1. Overrides the desk UI with a multi-theme visual system (CSS + JS bundle).
2. Hides the **Quality Management** and **Subcontracting** modules app-wide (URL/API/bootinfo/permissions).
3. Maintains a custom **Taxes Az** workspace on every migrate.
Installed into `frappe-bench` as a normal app. Python code is at `jey_theme/` (Frappe app package). Frontend assets live at `jey_theme/public/{css,js}`.
## Commands
Run from `frappe-bench/` (the bench root), not this app dir.
- `bench build --app jey_theme` — rebuild JS bundle after editing `jey_theme/public/js/jey_theme.js`.
- CSS-only changes: no build needed, just hard-reload the browser.
- `bench migrate` — runs the `after_migrate` hook that rebuilds the Taxes Az workspace from `setup/taxes_workspace.py`.
- `bench execute jey_theme.setup.taxes_workspace.setup` — re-run the workspace setup without a full migrate.
- `bench restart` — reload Python workers after editing `access_control.py` or `hooks.py`.
- No test suite, no linter config in this repo.
## Architecture
### Multi-theme system
### Python: access control (`jey_theme/access_control.py` + `hooks.py`)
The app supports multiple visual themes via `data-jey-theme` attribute on `<html>`.
Goal: make blocked modules invisible and unreachable for every user, including Administrator.
- Themes: **chrome** (metallic rings), **modern** (frosted glass)
- Theme is stored in `localStorage("jey-theme")` and set on page load
- Default theme: `chrome`
- No dark/light variants — each theme is self-contained
- Switching themes rebuilds icon DOM with per-theme SVGs
- `BLOCKED_MODULES` / `BLOCKED_DOCTYPES_EXTRA` — edit these frozensets to change what's blocked.
- Three defense layers wired in `hooks.py`:
- `before_request` — blocks `/api/resource/<Doctype>`, `/app/<slug>`, and form_dict-based calls at the HTTP boundary (catches Administrator, who bypasses `has_permission`).
- `has_permission = {"*": ...}` — per-doc deny for non-admin users who have role access.
- `extend_bootinfo` — strips blocked items from `allowed_modules`, `modules`, `user.can_*`, `workspaces.pages`, `workspace_sidebar_item`, `module_wise_workspaces`, `app_data`, `desktop_icons`, etc., so the client router/sidebar never sees them.
- Slug/doctype/workspace sets are cached on `frappe.local` per-request.
- When adding a new block vector: extend `extend_bootinfo` rather than relying only on `has_permission`, because the client has many independent boot-derived UIs.
### File structure
### Python: Taxes Az workspace (`jey_theme/setup/taxes_workspace.py`)
```
css/
shared.css — checkboxes, icon animations, base desktop-icon sizing, theme switcher UI
theme_chrome.css — chrome theme visuals
theme_modern.css — modern theme visuals
js/
jey_theme.js — theme init, per-theme icon maps (THEME_ICONS), DOM building, theme switcher override
public/html/
erpnext_chrome_menu_v2(15).html — reference design for chrome theme
```
Idempotent setup run via `after_migrate`.
### Adding a new theme
- Builds a `Workspace` named **Taxes Az** with three cards (Declarations, Setup, Tools), a `Workspace Sidebar` listing every taxes_az doctype, and a `Desktop Icon` that routes via the sidebar.
- The doctype list is a **static snapshot** in `DECLARATIONS_DOCTYPES` / `SETUP_DOCTYPES` / `TOOLS_DOCTYPES`. Adding a DocType to the `taxes_az` app does NOT auto-include it — append to the tuple and re-migrate.
- `_cleanup_obsolete()` removes legacy docs owned by this app (currently `"Taxes"`). Only touches records where `app == "jey_theme"` — never deletes ERPNext/other-app docs.
1. Create `css/theme_mytheme.css` with `[data-jey-theme="mytheme"]` selectors
2. Prefix keyframes with theme name: `jey-mytheme-*`
3. Add icon set in `THEME_ICONS.mytheme` in `jey_theme.js` (falls back to chrome icons if key missing)
4. Add entry to `JEY_THEMES` array in the switcher section
5. Add CSS file to `app_include_css` list in `hooks.py`
### Frontend: multi-theme system
### Theme scope
Bundle entry: `jey_theme/public/js/jey_theme.js`. CSS: `jey_theme/public/css/{shared,theme_chrome,theme_modern}.css`. All four are loaded via `app_include_css` / `app_include_js` in `hooks.py` for both desk and web contexts.
Themes can change **everything** — not just icons:
- CSS (`[data-jey-theme="..."]`): layout, positioning, colors, fonts, visibility, animations — any element on any page
- JS (`jeyRebuildIcons` / theme switch hooks): DOM restructuring, moving elements between containers, adding/removing components
- Per-theme SVG icon sets, per-theme DOM structure — all supported
- Theme selector lives in `data-jey-theme` on `<html>`, stored in `localStorage("jey-theme")`. Default: `chrome`. Set before first paint by an IIFE at the top of `jey_theme.js`.
- Themes supported: **chrome** (metallic rings), **modern** (frosted glass). No dark/light variants — each theme is self-contained.
- Theme-specific CSS MUST be wrapped in `[data-jey-theme="themename"]`. Keyframes MUST be prefixed `jey-<theme>-*` to avoid cross-theme name collisions.
- Shared CSS (checkboxes, icon base sizing, switcher UI, column-resize rules) lives in `shared.css` without theme selectors.
### Desktop icons
### Frontend: per-theme icons
- JS replaces `<img class="app-icon">` with inline `<svg class="jey-icon">` per current theme
- Each theme has its own `THEME_ICONS[themeName]` map with unique SVG paths
- Folder icons (`.folder-icon`) are converted to regular themed icons
- SVGs can have `cls` (e.g. `ico-buying`) for shared animations
- On theme switch, `jeyRebuildIcons()` tears down and rebuilds all icon DOM
- `THEME_ICONS[themeName]` in `jey_theme.js` maps icon keys to inline SVG path strings. Missing keys fall back to `THEME_ICONS.chrome`, then `_fallback`.
- JS replaces `<img class="app-icon">` / `.folder-icon` with inline `<svg class="jey-icon">` per current theme.
- `window.jeyRebuildIcons()` tears down and rebuilds all icon DOM — called on theme switch.
- SVGs may carry a `cls` (e.g. `ico-buying`) for shared CSS animations.
### Frontend: early-paint tricks in `jey_theme.js`
The top IIFE runs before paint to avoid FOUC:
- Sets `data-jey-theme` immediately.
- Defaults `container_fullwidth = "true"` on fresh install.
- Reads all `jey-col-widths:<Doctype>:<field>` localStorage keys and injects a `<style id="jey-col-resize-styles">` with per-grid column widths, plus a MutationObserver that stamps `data-jey-grid` on grids as they appear.
Keep this IIFE minimal and synchronous — anything slow here delays first paint.
## Adding a new theme
1. Create `jey_theme/public/css/theme_mytheme.css` with `[data-jey-theme="mytheme"]` selectors. Prefix keyframes `jey-mytheme-*`.
2. Add `THEME_ICONS.mytheme = { ... }` in `jey_theme.js` (only override keys you care about).
3. Add an entry to `JEY_THEMES` array in the switcher section of `jey_theme.js`.
4. Add the CSS file to both `app_include_css` and `web_include_css` in `hooks.py`.
5. `bench build --app jey_theme`.
## Rules
- Theme-specific CSS must be wrapped in `[data-jey-theme="themename"]` selector.
- Shared CSS (checkboxes, animations) lives in `shared.css` without theme selectors.
- CSS-only changes: clear browser cache. JS changes: `bench build --app jey_theme`.
- Themes can change anything (layout, positioning, colors, fonts, visibility, animations, DOM structure) — not just icons. Use JS (`jeyRebuildIcons` / theme-switch hooks) for DOM restructuring.
- Never add `[data-theme="dark"]` or light/dark variant rules — themes are self-contained (no inherited dark/light axis).
- Don't touch ERPNext-owned records in setup scripts; gate deletes on `app == "jey_theme"`.

View File

@ -305,10 +305,12 @@ input.list-header-checkbox:checked::after {
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Note: `to` intentionally omits `transform` leaving `translateY(0)` in the
final frame would keep a stacking context on every section after animation
completes, clipping Link-field autocomplete dropdowns behind later sections. */
.form-layout .form-section .section-head,
.form-layout .form-section .section-body {
animation: jey-section-in 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
@ -377,13 +379,77 @@ input.list-header-checkbox:checked::after {
visibility: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0.2s;
}
.dropdown-menu.show {
visibility: visible;
opacity: 1;
pointer-events: auto;
animation: jey-dropdown-in 0.2s ease;
}
/* Applied by JS during Bootstrap hide so Popper's transform reset
doesn't move the menu mid-fade. See "Dropdown fade-out" in jey_theme.js. */
.dropdown-menu.jey-dropdown-hiding {
visibility: visible !important;
pointer-events: none;
animation: jey-dropdown-out 0.2s ease forwards;
}
@keyframes jey-dropdown-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes jey-dropdown-out {
from { opacity: 1; }
to { opacity: 0; }
}
/* ==========================================================================
Sidebar Smooth Collapse / Expand
Frappe toggles .body-sidebar-placeholder between display:none and display:flex,
so the container's fit-content width snaps on expand (the inner sidebar goes
position:absolute, placeholder appears at 220px instantly). The collapse path
keeps the sidebar in flow, so its width transition is already smooth.
Override the container with an explicit width transition to make expand
symmetric with collapse.
========================================================================== */
@media (min-width: 576px) {
.body-sidebar-container {
width: 50px;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.body-sidebar-container.expanded {
width: var(--sidebar-width);
}
}
/* ==========================================================================
Sidebar No Alignment Shift Between States
Regular items land at 15px from the sidebar's left edge (sidebar padding 8
+ icon padding 7). Two state-dependent paddings break that alignment in
expanded mode:
1. Frappe's sidebar_header.js sets inline padding-left/right: 8px on
.sidebar-header when expanded, 0 when collapsed pushes the logo to
23px in expanded. Override inline style with !important.
2. SCSS mixin body-sidebar-expanded gives .sidebar-user-button
padding: var(--padding-sm) only when .expanded pushes the avatar
to 23px and makes the button taller, which shifts the Jey buttons
above it vertically. Zero out the padding.
Zeroing both keeps every icon/avatar at 15px and button heights constant
across expand/collapse, so nothing jumps on toggle.
========================================================================== */
.body-sidebar .sidebar-header {
padding-left: 0 !important;
padding-right: 0 !important;
}
.body-sidebar-container.expanded .sidebar-user-button {
padding: 0 !important;
}
/* ==========================================================================
@ -535,10 +601,13 @@ input.list-header-checkbox:checked::after {
transform: rotate(-90deg);
}
/* Expanded section-body has no `overflow: hidden` so Link-field autocomplete
dropdowns (and any other overflow) are not clipped by the section boundary.
`overflow: hidden` is applied via JS only during the collapse/expand
transition (see jey_theme.js), then removed once the animation finishes. */
.form-section .section-head.collapsible + .section-body {
display: flex !important;
flex-wrap: wrap;
overflow: hidden;
transition: max-height 0.5s ease-in-out,
padding 0.5s ease-in-out;
}
@ -547,6 +616,7 @@ input.list-header-checkbox:checked::after {
max-height: 0 !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
overflow: hidden;
transition: max-height 0.5s ease-in-out,
padding 0.5s ease-in-out;
}
@ -561,29 +631,51 @@ input.list-header-checkbox:checked::after {
animation: jey-awesomplete-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}
/* Applied by JS on awesomplete-close so the listbox fades instead of
vanishing when [hidden] is set. See "Awesomplete fade-out" in jey_theme.js. */
.awesomplete > [role="listbox"].jey-awesomplete-hiding {
display: block !important;
pointer-events: none;
animation: jey-awesomplete-out 0.2s ease forwards;
}
@keyframes jey-awesomplete-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes jey-awesomplete-out {
from { opacity: 1; }
to { opacity: 0; }
}
/* Lift the section containing an open autocomplete above its siblings so the
dropdown can spill past the section boundary without being painted over by
later sections (which naturally paint above earlier ones in DOM order). */
.form-layout .form-section:focus-within {
position: relative;
z-index: 10;
}
/* ==========================================================================
Input Focus Inset Press
========================================================================== */
.form-control,
.frappe-control input,
.frappe-control textarea,
.frappe-control select {
transition: box-shadow 0.4s ease, border-color 0.4s ease;
input.form-control:not([type="checkbox"]):not([type="radio"]),
textarea.form-control,
.frappe-control input:not([type="checkbox"]):not([type="radio"]),
.frappe-control textarea {
transition:
background-color 0.45s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.form-control:focus,
.frappe-control input:focus,
.frappe-control textarea:focus,
.frappe-control select:focus {
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1) !important;
border-color: var(--gray-400, #c0c6cc) !important;
input.form-control:not([type="checkbox"]):not([type="radio"]):focus,
textarea.form-control:focus,
.frappe-control input:not([type="checkbox"]):not([type="radio"]):focus,
.frappe-control textarea:focus {
border-color: var(--gray-500, #a8b0b8) !important;
outline: none;
}

View File

@ -366,10 +366,15 @@
2px 2px 4px rgba(0,0,0,0.2),
-1px -1px 2px rgba(255,255,255,0.15),
inset 0 1px 0 rgba(255,255,255,0.3),
inset 0 -1px 0 rgba(0,0,0,0.15) !important;
inset 0 -1px 0 rgba(0,0,0,0.15),
inset 2px 2px 5px rgba(0,0,0,0),
inset -1px -1px 2px rgba(255,255,255,0) !important;
text-shadow: 0 -1px 0 rgba(0,0,0,0.25);
position: relative;
overflow: hidden;
transition:
box-shadow 0.25s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.25s cubic-bezier(0.22, 1, 0.36, 1) !important;
}
[data-jey-theme="chrome"] .btn-primary::before {
@ -392,14 +397,21 @@
2px 2px 6px rgba(0,0,0,0.25),
-1px -1px 3px rgba(255,255,255,0.2),
inset 0 1px 0 rgba(255,255,255,0.35),
inset 0 -1px 0 rgba(0,0,0,0.15) !important;
inset 0 -1px 0 rgba(0,0,0,0.15),
inset 2px 2px 5px rgba(0,0,0,0),
inset -1px -1px 2px rgba(255,255,255,0) !important;
}
[data-jey-theme="chrome"] .btn-primary:active {
background: linear-gradient(45deg, #555 5%, #888 10%, #777 30%, #888 50%, #777 70%, #888 85%, #555 95%) !important;
box-shadow:
inset 2px 2px 4px rgba(0,0,0,0.25),
inset -1px -1px 2px rgba(255,255,255,0.1) !important;
2px 2px 4px rgba(0,0,0,0),
-1px -1px 2px rgba(255,255,255,0),
inset 0 1px 0 rgba(255,255,255,0),
inset 0 -1px 0 rgba(0,0,0,0.05),
inset 3px 3px 7px rgba(0,0,0,0.35),
inset -1px -1px 2px rgba(255,255,255,0.12) !important;
transition: box-shadow 0.08s ease-out !important;
}
@ -416,8 +428,13 @@
1px 1px 3px rgba(0,0,0,0.12),
-1px -1px 2px rgba(255,255,255,0.4),
inset 0 1px 0 rgba(255,255,255,0.6),
inset 0 -1px 0 rgba(0,0,0,0.06) !important;
inset 0 -1px 0 rgba(0,0,0,0.06),
inset 2px 2px 4px rgba(0,0,0,0),
inset -1px -1px 1px rgba(255,255,255,0) !important;
text-shadow: 0 1px 0 rgba(255,255,255,0.5);
transition:
box-shadow 0.25s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.25s cubic-bezier(0.22, 1, 0.36, 1) !important;
}
[data-jey-theme="chrome"] .btn-default:hover,
@ -428,15 +445,22 @@
2px 2px 4px rgba(0,0,0,0.15),
-1px -1px 2px rgba(255,255,255,0.4),
inset 0 1px 0 rgba(255,255,255,0.7),
inset 0 -1px 0 rgba(0,0,0,0.06) !important;
inset 0 -1px 0 rgba(0,0,0,0.06),
inset 2px 2px 4px rgba(0,0,0,0),
inset -1px -1px 1px rgba(255,255,255,0) !important;
}
[data-jey-theme="chrome"] .btn-default:active,
[data-jey-theme="chrome"] .btn-secondary:active {
background: linear-gradient(45deg, #aaa 5%, #d8d8d8 10%, #bbb 30%, #ccc 50%, #bbb 70%, #d8d8d8 85%, #aaa 95%) !important;
box-shadow:
inset 2px 2px 3px rgba(0,0,0,0.15),
inset -1px -1px 2px rgba(255,255,255,0.2) !important;
1px 1px 3px rgba(0,0,0,0),
-1px -1px 2px rgba(255,255,255,0),
inset 0 1px 0 rgba(255,255,255,0),
inset 0 -1px 0 rgba(0,0,0,0.05),
inset 3px 3px 6px rgba(0,0,0,0.3),
inset -1px -1px 1px rgba(255,255,255,0.15) !important;
transition: box-shadow 0.08s ease-out !important;
}
/* Danger */
@ -465,28 +489,34 @@
Input Fields Sunken Metal Inset
========================================================================== */
[data-jey-theme="chrome"] .form-control {
background: linear-gradient(180deg, #c8c8c8 0%, #d5d5d5 100%) !important;
[data-jey-theme="chrome"] input.form-control:not([type="checkbox"]):not([type="radio"]),
[data-jey-theme="chrome"] textarea.form-control {
background: #d0d0d0 !important;
border: 1px solid #999 !important;
box-shadow:
inset 2px 2px 4px rgba(0,0,0,0.12),
inset -1px -1px 2px rgba(255,255,255,0.2),
0 1px 0 rgba(255,255,255,0.5) !important;
color: #333 !important;
box-shadow:
inset 1px 1px 2px rgba(0,0,0,0.10),
inset -1px -1px 1px rgba(255,255,255,0.25),
inset 0 1px 1px rgba(0,0,0,0.04) !important;
transition:
background-color 0.45s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.45s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.45s cubic-bezier(0.22, 1, 0.36, 1) !important;
}
[data-jey-theme="chrome"] .form-control:focus {
background: linear-gradient(180deg, #d5d5d5 0%, #e0e0e0 100%) !important;
[data-jey-theme="chrome"] input.form-control:not([type="checkbox"]):not([type="radio"]):focus,
[data-jey-theme="chrome"] textarea.form-control:focus {
background: #cbcbcb !important;
border-color: #777 !important;
box-shadow:
inset 1px 1px 3px rgba(0,0,0,0.08),
0 0 0 2px rgba(120,120,120,0.25),
0 1px 0 rgba(255,255,255,0.4) !important;
inset 4px 4px 8px rgba(0,0,0,0.28),
inset -1px -1px 1px rgba(255,255,255,0.15),
inset 0 3px 5px rgba(0,0,0,0.18) !important;
}
[data-jey-theme="chrome"] .form-control:disabled,
[data-jey-theme="chrome"] .form-control[disabled] {
background: #c8c8c8 !important;
background: #d8d8d8 !important;
color: #888 !important;
box-shadow: inset 1px 1px 2px rgba(0,0,0,0.06) !important;
}
@ -499,7 +529,7 @@
[data-jey-theme="chrome"] select.form-control,
[data-jey-theme="chrome"] select.input-with-feedback {
background: linear-gradient(180deg, #c8c8c8 0%, #d5d5d5 100%) !important;
background: #d0d0d0 !important;
}
@ -659,10 +689,88 @@
}
[data-jey-theme="chrome"] .grid-footer {
background: linear-gradient(45deg, #b0b0b0 5%, #d0d0d0 20%, #bbb 50%, #d0d0d0 80%, #b0b0b0 95%) !important;
background: transparent !important;
border-top: 1px solid #999 !important;
}
/* --- Active (edit-mode) grid cells: identical look to the static row --- */
/* Keep the row's bottom border visible even when in edit mode */
[data-jey-theme="chrome"] .rows .grid-row.editable-row,
[data-jey-theme="chrome"] .rows .grid-row:has(.editable-row) {
border-bottom: 1px solid #b8b8b8 !important;
}
/* Flatten every form control inside a grid row — no bg, no border, no shadow */
[data-jey-theme="chrome"] .grid-row input.form-control:not([type="checkbox"]):not([type="radio"]),
[data-jey-theme="chrome"] .grid-row textarea.form-control,
[data-jey-theme="chrome"] .grid-row select.form-control,
[data-jey-theme="chrome"] .grid-row select.input-with-feedback,
[data-jey-theme="chrome"] .grid-row .awesomplete input,
[data-jey-theme="chrome"] .grid-row .like-disabled-input {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
outline: none !important;
color: #333 !important;
transition:
box-shadow 0.45s cubic-bezier(0.22, 1, 0.36, 1),
background 0.45s cubic-bezier(0.22, 1, 0.36, 1) !important;
}
/* Focused cell: pressed-in look — same shadow pattern as .btn-default:active */
[data-jey-theme="chrome"] .grid-row input.form-control:not([type="checkbox"]):not([type="radio"]):focus,
[data-jey-theme="chrome"] .grid-row textarea.form-control:focus,
[data-jey-theme="chrome"] .grid-row select.form-control:focus,
[data-jey-theme="chrome"] .grid-row select.input-with-feedback:focus,
[data-jey-theme="chrome"] .grid-row .awesomplete input:focus {
background: linear-gradient(45deg, #bbb 5%, #d8d8d8 10%, #c4c4c4 30%, #d0d0d0 50%, #c4c4c4 70%, #d8d8d8 85%, #bbb 95%) !important;
border: 0 !important;
outline: none !important;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0),
inset 0 -1px 0 rgba(0,0,0,0.05),
inset 3px 3px 6px rgba(0,0,0,0.3),
inset -1px -1px 1px rgba(255,255,255,0.15) !important;
transition:
box-shadow 0.45s cubic-bezier(0.22, 1, 0.36, 1),
background 0.45s cubic-bezier(0.22, 1, 0.36, 1) !important;
}
/* Read-only cells darker grey bg. Specificity matches the "flatten" rule above
(tag.class + two :not()), so it wins over `background: transparent`. Frappe hides
.field-area on inactive rows, so the tint only shows on the currently-active row. */
[data-jey-theme="chrome"] .grid-row input.form-control:not([type="checkbox"]):not([type="radio"]):disabled,
[data-jey-theme="chrome"] .grid-row input.form-control:not([type="checkbox"]):not([type="radio"])[disabled],
[data-jey-theme="chrome"] .grid-row input.form-control:not([type="checkbox"]):not([type="radio"])[readonly],
[data-jey-theme="chrome"] .grid-row textarea.form-control:disabled,
[data-jey-theme="chrome"] .grid-row textarea.form-control[readonly],
[data-jey-theme="chrome"] .grid-row select.form-control:disabled,
[data-jey-theme="chrome"] .grid-row select.input-with-feedback:disabled {
background: #d8d8d8 !important;
box-shadow: none !important;
color: #666 !important;
}
/* No pressed-in effect on focus for read-only cells */
[data-jey-theme="chrome"] .grid-row input.form-control:not([type="checkbox"]):not([type="radio"]):disabled:focus,
[data-jey-theme="chrome"] .grid-row input.form-control:not([type="checkbox"]):not([type="radio"])[disabled]:focus,
[data-jey-theme="chrome"] .grid-row input.form-control:not([type="checkbox"]):not([type="radio"])[readonly]:focus,
[data-jey-theme="chrome"] .grid-row textarea.form-control:disabled:focus,
[data-jey-theme="chrome"] .grid-row textarea.form-control[readonly]:focus,
[data-jey-theme="chrome"] .grid-row select.form-control:disabled:focus {
background: #d8d8d8 !important;
box-shadow: none !important;
}
/* Match static-row cell height: static cells use padding 10px 8px; form-control in
Frappe uses padding-top:10px, padding-bottom: calc(--padding-md - 3px). Equalise so
the active row doesn't grow or shrink compared to an idle row. */
[data-jey-theme="chrome"] .grid-row .editable-row .form-control {
padding-top: 10px !important;
padding-bottom: 10px !important;
height: auto !important;
}
/* ==========================================================================
Tabs Metallic Tab Strip

View File

@ -1498,9 +1498,25 @@
// Set exact max-height so transition has a real start/end value
body.style.maxHeight = body.scrollHeight + "px";
// Clip overflow during the transition so expanding content doesn't
// spill out before the section has grown to fit it. Removed at
// transitionend so Link-field dropdowns can escape section bounds.
body.style.overflow = "hidden";
// Force reflow — browser must register this value before Frappe toggles .collapsed
void body.offsetHeight;
}, true);
document.addEventListener("transitionend", function (e) {
var body = e.target;
if (!body.classList || !body.classList.contains("section-body")) return;
if (e.propertyName !== "max-height") return;
var head = body.previousElementSibling;
if (!head || !head.classList.contains("collapsible")) return;
// After expand: drop the inline overflow so dropdowns can escape.
// After collapse: the `.collapsed + .section-body` CSS rule keeps
// overflow hidden, so clearing the inline value is still safe.
body.style.overflow = "";
}, true);
})();
@ -1562,3 +1578,96 @@
});
bodyObserver.observe(document.body, { childList: true, subtree: true });
})();
/* ==========================================================================
Dropdown fade-out freeze Popper position during hide animation
Bootstrap removes .show and then Popper.destroy() wipes the menu's
inline transform/top/left, so a CSS transition would fade the menu at
(0,0) instead of its visible spot. On hide.bs.dropdown we snapshot the
computed position, then re-apply it in a microtask (after Bootstrap's
synchronous cleanup) together with the .jey-dropdown-hiding class that
plays the opacity animation. Cleanup on animation end or next show.
========================================================================== */
(function () {
if (typeof window.jQuery === "undefined") return;
function cleanup(menu) {
menu.classList.remove("jey-dropdown-hiding");
menu.style.removeProperty("transform");
menu.style.removeProperty("top");
menu.style.removeProperty("left");
menu.style.removeProperty("position");
if (menu._jeyHideTimer) {
clearTimeout(menu._jeyHideTimer);
menu._jeyHideTimer = null;
}
}
jQuery(document).on("hide.bs.dropdown", function (e) {
var menu = e.target && e.target.querySelector(".dropdown-menu.show");
if (!menu) return;
var cs = getComputedStyle(menu);
var transform = cs.transform;
var top = cs.top;
var left = cs.left;
var position = cs.position;
Promise.resolve().then(function () {
menu.style.setProperty("transform", transform, "important");
menu.style.setProperty("top", top, "important");
menu.style.setProperty("left", left, "important");
menu.style.setProperty("position", position, "important");
menu.classList.add("jey-dropdown-hiding");
menu._jeyHideTimer = setTimeout(function () {
cleanup(menu);
}, 220);
});
});
// If the dropdown reopens before fade-out finishes, clear our frozen
// styles so Popper can position it fresh.
jQuery(document).on("show.bs.dropdown", function (e) {
var menu = e.target && e.target.querySelector(".dropdown-menu");
if (!menu) return;
cleanup(menu);
});
})();
/* ==========================================================================
Awesomplete (link field) fade-out
Awesomplete.close() just toggles the [hidden] attribute, so the listbox
vanishes with no transition. On awesomplete-close we tag the ul with
.jey-awesomplete-hiding to play a fade-out animation, and clear the tag
on re-open (or after the animation finishes) so the in-animation can
play cleanly next time.
========================================================================== */
(function () {
if (typeof window.jQuery === "undefined") return;
function findListbox(input) {
var wrapper = input && input.closest && input.closest(".awesomplete");
return wrapper ? wrapper.querySelector('ul[role="listbox"]') : null;
}
jQuery(document).on("awesomplete-close", function (e) {
var ul = findListbox(e.target);
if (!ul) return;
ul.classList.add("jey-awesomplete-hiding");
if (ul._jeyHideTimer) clearTimeout(ul._jeyHideTimer);
ul._jeyHideTimer = setTimeout(function () {
ul.classList.remove("jey-awesomplete-hiding");
ul._jeyHideTimer = null;
}, 220);
});
jQuery(document).on("awesomplete-open", function (e) {
var ul = findListbox(e.target);
if (!ul) return;
ul.classList.remove("jey-awesomplete-hiding");
if (ul._jeyHideTimer) {
clearTimeout(ul._jeyHideTimer);
ul._jeyHideTimer = null;
}
});
})();