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:
parent
5cbc36566b
commit
953bf8b2c8
107
CLAUDE.md
107
CLAUDE.md
|
|
@ -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"`.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in New Issue