Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions assets/css/easyadmin-theme/action-menu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/* ActionMenu (dropdown)
/* ------------------------------------------------------------------------- */
/* Open/close animation, selectable (radio/checkbox) items and submenu chevron,
modeled on the shadcn/ui Dropdown Menu. */

.dropdown-menu {
--ea-action-menu-slide: 8px;
}

/* ENTER: triggered when Bootstrap adds the `.show` class. We animate the
individual `scale`/`translate` properties (not `transform`) so they compose
on top of Popper's inline `transform: translate()` positioning instead of
clobbering it. `transform-origin` is anchored to the corner nearest the
toggle, mimicking shadcn's --radix-*-transform-origin. */
.dropdown-menu.show {
/* `ease` (the CSS default) is exactly what shadcn/tw-animate-css uses. A strong
decelerate curve (e.g. --ea-ease-out) has a long, slow tail that reads as sluggish
at this short duration, so we deliberately match shadcn's `ease` here */
animation: ea-action-menu-enter var(--ea-duration-default) ease;
}
@keyframes ea-action-menu-enter {
from {
opacity: 0;
scale: 0.95;
translate: var(--ea-enter-x, 0) var(--ea-enter-y, 0);
}
to {
opacity: 1;
scale: 1;
translate: 0 0;
}
}
.dropdown-menu[data-popper-placement^="bottom"] {
transform-origin: top center;
--ea-enter-y: calc(-1 * var(--ea-action-menu-slide));
}
.dropdown-menu[data-popper-placement^="top"] {
transform-origin: bottom center;
--ea-enter-y: var(--ea-action-menu-slide);
}
.dropdown-menu[data-popper-placement^="left"] {
transform-origin: right center;
--ea-enter-x: var(--ea-action-menu-slide);
}
.dropdown-menu[data-popper-placement^="right"] {
transform-origin: left center;
--ea-enter-x: calc(-1 * var(--ea-action-menu-slide));
}
.dropdown-menu[data-popper-placement$="-end"] {
transform-origin: top right;
}
.dropdown-menu[data-popper-placement$="-start"] {
transform-origin: top left;
}

/* EXIT: Bootstrap removes `.show` and destroys Popper synchronously, so a
CSS-only exit can't play. page-layout.js adds `.ea-dropdown-hiding` while
`.show` (and Popper positioning) is still in place, plays this, then lets
Bootstrap finish hiding */
.dropdown-menu.ea-dropdown-hiding {
animation: ea-action-menu-exit var(--ea-duration-fast) var(--ea-ease-in) forwards;
pointer-events: none;
}
@keyframes ea-action-menu-exit {
from {
opacity: 1;
scale: 1;
}
to {
opacity: 0;
scale: var(--ea-action-menu-zoom-from);
}
}

@media (prefers-reduced-motion: reduce) {
.dropdown-menu.show,
.dropdown-menu.ea-dropdown-hiding {
animation: none;
}
}

/* Selectable items (RadioList / CheckboxList wrapping normal Items with the
`selected` prop): the item keeps its own leading icon, and a check is shown
on the trailing edge. The check's visibility is driven by aria-checked, so
JavaScript only toggles the attribute, never the element */
.dropdown-menu .dropdown-selectable-group {
list-style: none;
margin: 0;
padding: 0;
}
.dropdown-menu .dropdown-item-trailing {
margin-inline-start: auto;
display: inline-flex;
align-items: center;
}
.dropdown-menu .dropdown-item-check {
color: var(--color-primary);
inline-size: 16px;
block-size: 16px;
opacity: 0;
transition: opacity var(--ea-duration-fast) var(--ea-ease-out);
}
.dropdown-menu .dropdown-item[aria-checked="true"] .dropdown-item-check,
.dropdown-menu .dropdown-item-selectable.active .dropdown-item-check {
opacity: 1;
}

/* Selected state = check + subtle background (softens the previous "bland"
highlight: drops the inset ring, keeps the leading icon in its own color and
tints only the trailing check). Specificity 3 matches the
`.dropdown-settings .dropdown-item.active` rule in base.css and wins by being
loaded later. */
.dropdown-menu .dropdown-item-selectable.active,
.dropdown-menu .dropdown-item-selectable[aria-checked="true"] {
background: var(--dropdown-settings-active-item-bg);
box-shadow: none;
color: var(--dropdown-link-color);
}
.dropdown-menu .dropdown-item-selectable.active .icon,
.dropdown-menu .dropdown-item-selectable[aria-checked="true"] .icon {
color: var(--dropdown-icon-color);
}
.dropdown-menu .dropdown-item-selectable.active .dropdown-item-check {
color: var(--color-primary);
}

/* submenu chevron (replaces the old CSS-border triangle). shadcn renders a muted
chevron-right pushed to the trailing edge as the "has submenu" affordance. */
.dropdown-submenu .dropdown-toggle::after,
.dropdown-submenu .dropdown-toggle::before {
display: none; /* hide Bootstrap's native caret; we render a chevron icon instead */
}
.dropdown-menu .dropdown-submenu-chevron {
margin-inline-start: auto;
color: var(--text-muted);
inline-size: 16px;
block-size: 16px;
}
/* the chevron follows the side the submenu actually opens to: right by default
(chevron-right, no flip), flipped to point left when the submenu opens to the
left (Bootstrap's .dropstart, e.g. datagrid row actions) */
.dropstart > .dropdown-submenu-chevron,
.dropstart > * .dropdown-submenu-chevron {
scale: -1 1; /* point left */
order: -1; /* and sit on the leading (left) edge, the side it opens to */
margin-inline-start: 0;
}
35 changes: 28 additions & 7 deletions assets/css/easyadmin-theme/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -707,24 +707,28 @@ a.user-menu-wrapper .user-details:hover {

.dropdown-menu {
--dropdown-padding: 4px;
--dropdown-radius: var(--border-radius-lg); /* 8px */
--dropdown-item-radius: var(--border-radius-lg); /* 8px */

background-color: var(--dropdown-bg);
border-color: var(--dropdown-border-color);
border-radius: var(--dropdown-radius);
box-shadow: var(--shadow-xl);
color: var(--dropdown-color);
min-inline-size: 8rem;
max-inline-size: 240px;
padding: 5px;
padding: var(--dropdown-padding);
}
.dropdown-menu.dropdown-has-submenus {
padding-inline-start: 25px;
}
.dropdown-menu li {
border-radius: var(--border-radius);
border-radius: var(--dropdown-item-radius);
}
.dropdown-menu a,
.dropdown-menu a:hover,
.dropdown-menu a:active {
border-radius: var(--border-radius);
border-radius: var(--dropdown-item-radius);
color: var(--dropdown-link-color);
}
.dropdown-menu a:hover {
Expand All @@ -733,25 +737,42 @@ a.user-menu-wrapper .user-details:hover {
.dropdown-menu i,
.dropdown-menu .icon {
color: var(--dropdown-icon-color);
margin: 0 8px 0 0;
font-size: 15px;
margin: 0;
/* 14px (not 16px): FontAwesome's filled glyphs read heavier than shadcn's thin SVG
strokes, so a slightly smaller size matches the visual weight. Drives both the FA <i>
and the 1em cap on SVG icons, so every dropdown icon scales together. */
font-size: 14px;
}
.dropdown-menu .icon i {
margin: 0;
line-height: 1;
}
.dropdown-menu .icon {
display: inline-flex;
align-items: center;
justify-content: center;
/* fixed-width icon column following FontAwesome's .fa-fw (1.25em): every FA glyph is
designed to fit within 1.25em, so icons render full-size, stay centered, never get
clipped, and labels line up. SVG icons are capped at 1em and centered in the same
column. */
inline-size: 1.25em;
block-size: 16px;
flex-shrink: 0;
/* reset the item's 20px line-height so glyphs aren't inflated vertically */
line-height: 1;
}

.dropdown-menu .dropdown-item,
.dropdown-menu .dropdown-header {
align-items: center;
display: flex;
block-size: 28px;
gap: 6px;
min-block-size: 28px;
white-space: nowrap;
overflow: hidden;
padding: 0 12px 0 6px;
padding: 4px 6px;
font-size: var(--font-size-base);
line-height: 1.25rem; /* 28px */
text-overflow: ellipsis;
}
.dropdown-menu .dropdown-divider {
Expand Down
14 changes: 0 additions & 14 deletions assets/css/easyadmin-theme/datagrids.css
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,6 @@ table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dro
position: relative;
}

.datagrid .dropdown-actions .dropstart .dropdown-toggle:before {
margin-inline-start: -20px;
position: absolute;
}

/* Position nested dropdown menu to the left */
.datagrid .dropdown-actions .dropdown-menu .dropstart > .dropdown-menu {
inset-block-start: 0;
Expand Down Expand Up @@ -329,15 +324,6 @@ table.datagrid:not(.datagrid-empty) tr:not(.empty-row) td.actions.actions-as-dro
display: none;
}

.datagrid .dropdown-actions .dropdown-menu .dropstart .dropdown-toggle-split .dropdown-toggle-marker {
/* this is a chevron-like icon made out of text with CSS border tricks */
border-block-end: .3em solid transparent;
border-inline-end: .3em solid;
border-block-start: .3em solid transparent;
content: "";
display: inline-block;
}

.datagrid .ea-lightbox-thumbnail img {
background: var(--white);
border: 1px solid transparent;
Expand Down
1 change: 1 addition & 0 deletions assets/css/easyadmin-theme/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@import "./variables-theme.css";

@import "./base.css";
@import "./action-menu.css";
@import "./menu.css";
@import "./datagrids.css";
@import "./detail-page.css";
Expand Down
21 changes: 11 additions & 10 deletions assets/js/page-color-scheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ class ColorSchemeHandler {
const colorSchemeSelectors = document.querySelectorAll('.dropdown-settings');
const currentScheme = localStorage.getItem(this.#colorSchemeLocalStorageKey) || 'auto';

// toggle the selected state via aria-checked (this drives the trailing checkmark
// shown by CSS); the legacy `active` class is kept in sync for one release
const setChecked = (el, isChecked) => {
el.setAttribute('aria-checked', isChecked ? 'true' : 'false');
el.classList.toggle('active', isChecked);
};

colorSchemeSelectors.forEach((colorSchemeSelector) => {
const selectorOptions = colorSchemeSelector.querySelectorAll(
'a.dropdown-appearance-item[data-ea-color-scheme]'
Expand All @@ -30,10 +37,8 @@ class ColorSchemeHandler {
`a.dropdown-appearance-item[data-ea-color-scheme="${currentScheme}"]`
);

selectorOptions.forEach((selector) => {
selector.classList.remove('active');
});
selectorActiveOption.classList.add('active');
selectorOptions.forEach((selector) => setChecked(selector, false));
setChecked(selectorActiveOption, true);

selectorOptions.forEach((selector) => {
selector.addEventListener('click', () => {
Expand All @@ -46,12 +51,8 @@ class ColorSchemeHandler {
const allSelectorActiveOptions = document.querySelectorAll(
`a.dropdown-appearance-item[data-ea-color-scheme="${selectedColorScheme}"]`
);
allSelectorOptions.forEach((selectorOption) => {
selectorOption.classList.remove('active');
});
allSelectorActiveOptions.forEach((selectorOption) => {
selectorOption.classList.add('active');
});
allSelectorOptions.forEach((selectorOption) => setChecked(selectorOption, false));
allSelectorActiveOptions.forEach((selectorOption) => setChecked(selectorOption, true));
});
});
});
Expand Down
48 changes: 48 additions & 0 deletions assets/js/page-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,51 @@ document.body.classList.add(
`ea-content-width-${localStorage.getItem('ea/content/width') || document.body.dataset.eaContentWidth}`,
`ea-sidebar-width-${localStorage.getItem('ea/sidebar/width') || document.body.dataset.eaSidebarWidth}`
);

// ActionMenu (Bootstrap dropdown) close animation.
// The enter animation is pure CSS (.dropdown-menu.show), but Bootstrap removes
// `.show` and destroys Popper synchronously when hiding, so a CSS-only exit can't
// play. We intercept `hide.bs.dropdown` (a native bubbling CustomEvent, so no
// Bootstrap import is needed here), play the CSS exit while `.show` and Popper
// positioning are still in place, then let Bootstrap finish hiding.
(() => {
const EXIT_CLASS = 'ea-dropdown-hiding';
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

const menuFor = (toggle) => {
const wrapper = toggle.closest('.dropdown, .dropstart, .dropend, .dropup');

return wrapper?.querySelector(':scope > .dropdown-overlay > .dropdown-menu, :scope > .dropdown-menu');
};

document.addEventListener('hide.bs.dropdown', (event) => {
if (reduceMotion || undefined === window.bootstrap) {
return; // hide instantly
}

const menu = menuFor(event.target);
if (!menu) {
return;
}

// The programmatic hide() re-fires this event; let it pass through.
if ('1' === menu.dataset.eaAllowHide) {
delete menu.dataset.eaAllowHide;
menu.classList.remove(EXIT_CLASS);

return;
}

event.preventDefault();
menu.classList.add(EXIT_CLASS);

const finish = () => {
clearTimeout(fallback);
menu.removeEventListener('animationend', finish);
menu.dataset.eaAllowHide = '1';
window.bootstrap.Dropdown.getOrCreateInstance(event.target).hide();
};
const fallback = setTimeout(finish, 250); // safety net if animationend never fires
menu.addEventListener('animationend', finish, { once: true });
});
})();
2 changes: 1 addition & 1 deletion public/app.5892cfa7.css → public/app.9786c891.css

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions public/entrypoints.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"entrypoints": {
"app": {
"css": [
"/app.5892cfa7.css"
"/app.9786c891.css"
],
"js": [
"/app.b016c66e.js"
Expand All @@ -15,12 +15,12 @@
},
"page-layout": {
"js": [
"/page-layout.6e9fe55d.js"
"/page-layout.696f1816.js"
]
},
"page-color-scheme": {
"js": [
"/page-color-scheme.75224563.js"
"/page-color-scheme.e9aaf2f0.js"
]
},
"field-boolean": {
Expand Down
6 changes: 3 additions & 3 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"app.css": "app.5892cfa7.css",
"app.css": "app.9786c891.css",
"app.js": "app.b016c66e.js",
"form.js": "form.bd28918c.js",
"page-layout.js": "page-layout.6e9fe55d.js",
"page-color-scheme.js": "page-color-scheme.75224563.js",
"page-layout.js": "page-layout.696f1816.js",
"page-color-scheme.js": "page-color-scheme.e9aaf2f0.js",
"field-boolean.js": "field-boolean.6eb3e3a7.js",
"field-code-editor.css": "field-code-editor.cdcf15eb.css",
"field-code-editor.js": "field-code-editor.a0cd3c60.js",
Expand Down
Loading
Loading