Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
dde7258
Gallery landing hub Initial layout
ramram-mf May 8, 2026
76eb976
Inline project carousel checkpoint
ramram-mf May 8, 2026
a90eb27
Refine gallery hub project carousel
ramram-mf May 8, 2026
27bbbaa
Polish gallery hub slide motion
ramram-mf May 8, 2026
9f6a3c7
Fix lint issues
ramram-mf May 8, 2026
9d7f75a
Merge branch 'main' into TP1-3744-gallery-hub-landing-page-frontend
ramram-mf May 8, 2026
5fc0dbd
Update migrations
ramram-mf May 8, 2026
d382703
Fix scroll from bottom issue.
ramram-mf May 11, 2026
e4d6eb0
Merge branch 'main' into TP1-3744-gallery-hub-landing-page-frontend
ramram-mf May 11, 2026
63dceec
Update gallery page factory to model shape.
ramram-mf May 11, 2026
ae152c0
Refine gallery hub project layout
ramram-mf May 11, 2026
68453a1
Format gallery hub settings
ramram-mf May 11, 2026
04a66e5
Merge branch 'main' into TP1-3744-gallery-hub-landing-page-frontend
ramram-mf May 12, 2026
eace21a
Add mobile gallery landing composition
ramram-mf May 12, 2026
452c28a
Refine gallery hub responsive layout
ramram-mf May 12, 2026
9a647f3
Audit gallery hub responsive behavior
ramram-mf May 12, 2026
d67d4b0
Add mobile gallery project layout
ramram-mf May 12, 2026
9edda0b
Refine gallery hub carousel indicators
ramram-mf May 12, 2026
be5ebac
Refine gallery hub carousel gestures
ramram-mf May 12, 2026
b394fc3
Format gallery hub gesture imports
ramram-mf May 12, 2026
fdd38ef
Catch up to main
ramram-mf May 13, 2026
d0912ca
Merge branch 'main' into TP1-3744-gallery-hub-landing-page-frontend
ramram-mf May 13, 2026
ee9e98b
Address gallery hub landing page feedback
ramram-mf May 14, 2026
c04c114
Refine gallery project media pagination
ramram-mf May 14, 2026
7d8ef7f
Guard gallery intro navigation during animation
ramram-mf May 14, 2026
2fa3766
Clean up gallery hub review findings
ramram-mf May 14, 2026
90373a8
Refine gallery hub mobile layout
ramram-mf May 14, 2026
7b9625f
Fix gallery hub SCSS formatting
ramram-mf May 14, 2026
5a28a1f
Merge branch 'main' into TP1-3744-gallery-hub-landing-page-frontend
ramram-mf May 14, 2026
920b2d5
Refine gallery project navigation layout
ramram-mf May 14, 2026
9ba8ea9
Address gallery hub PR feedback
ramram-mf May 14, 2026
9a6e426
Polish gallery hub project layout
ramram-mf May 15, 2026
2a1aebc
Reduce gallery hub intro entering duration from 1400ms to 1200ms
ramram-mf May 15, 2026
baf0e2b
Merge branch 'main' into TP1-3744-gallery-hub-landing-page-frontend
ramram-mf May 15, 2026
43f4a82
Refine gallery hub project layout
ramram-mf May 15, 2026
3a0c5b8
Center gallery hub intro artwork
ramram-mf May 15, 2026
c5bc502
Format gallery hub settings
ramram-mf May 15, 2026
196e4bc
Animate mobile gallery projects vertically
ramram-mf May 15, 2026
fc5fbc5
Refactor gallery hub layout tokens
ramram-mf May 15, 2026
452f864
Fix gallery hub lint
ramram-mf May 15, 2026
817bdb5
Tune gallery hub short viewport layout
ramram-mf May 15, 2026
aa281d7
Tune gallery hub artwork layout
ramram-mf May 16, 2026
ea174d9
Add gallery hub project list overlay
ramram-mf May 18, 2026
0e104d9
Address gallery hub review feedback
ramram-mf May 18, 2026
797416a
Fix gallery hub carousel formatting
ramram-mf May 18, 2026
0abd4ee
Merge remote-tracking branch 'origin/TP1-3744-gallery-hub-landing-pag…
ramram-mf May 18, 2026
2b55e6d
Animate gallery project list modal
ramram-mf May 19, 2026
5967582
Refine gallery project list sizing
ramram-mf May 19, 2026
fd44ed2
Update gallery hub project navigation
ramram-mf May 19, 2026
3e8ed7e
Merge branch 'TP1-3744-gallery-hub-landing-page-frontend' into TP1-38…
ramram-mf May 19, 2026
01f288d
Refine gallery hub short viewport layout
ramram-mf May 19, 2026
38a6d71
Document gallery hub carousel safeguards
ramram-mf May 19, 2026
fd8f5a9
Merge branch 'TP1-3744-gallery-hub-landing-page-frontend' into TP1-38…
ramram-mf May 19, 2026
858d995
Refine gallery hub project list modal
ramram-mf May 19, 2026
589325d
Merge branch 'main' into TP1-3744-gallery-hub-landing-page-frontend
ramram-mf May 19, 2026
7c3dcf4
Merge branch 'TP1-3744-gallery-hub-landing-page-frontend' into TP1-38…
ramram-mf May 19, 2026
0b28b38
Address gallery project list review feedback
ramram-mf May 19, 2026
9f080f4
Format gallery project list shape cycle
ramram-mf May 20, 2026
e817589
Merge branch 'main' into TP1-3744-gallery-hub-landing-page-frontend
ramram-mf May 20, 2026
1c43e00
Merge branch 'TP1-3744-gallery-hub-landing-page-frontend' into TP1-38…
ramram-mf May 20, 2026
fec3483
Fix gallery project list shape cycle
ramram-mf May 20, 2026
c0447c1
Restore gallery controls grid alignment
ramram-mf May 20, 2026
a6c75fb
Update gallery project list modal styles
ramram-mf May 20, 2026
e2915ec
Move gallery cookie launcher on mobile
ramram-mf May 20, 2026
70f873d
Merge branch 'TP1-3744-gallery-hub-landing-page-frontend' into TP1-38…
ramram-mf May 20, 2026
b8ad32c
Merge remote-tracking branch 'origin/main' into TP1-3891-gallery-hub-…
ramram-mf May 20, 2026
ce1c2b0
Refine gallery project list desktop styles
ramram-mf May 20, 2026
7fc25b3
Merge branch 'main' into TP1-3891-gallery-hub-project-list-modal
ramram-mf May 20, 2026
370a13e
Address gallery modal review feedback
ramram-mf May 20, 2026
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
5 changes: 5 additions & 0 deletions foundation_cms/gallery_hub/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ProgramLabel,
ProjectPageHeroMedia,
)
from foundation_cms.profiles.models import ExpertProfilePage

MULTI_HERO_PROJECT_INDEXES = {0, 3, 6}
ADDITIONAL_HERO_MEDIA_COUNT = 3
Expand Down Expand Up @@ -65,6 +66,9 @@ def generate(seed):
# --- Images (created earlier in load_redesign_data) ---
images = list(get_image_model().objects.all())

# --- Experts (created earlier in load_redesign_data) ---
experts = list(ExpertProfilePage.objects.live().filter(locale=default_locale))

# --- 1 Gallery Hub Page (directly under site root) ---
print("Creating Gallery Hub Page...")
existing_gallery = GalleryPage.objects.filter(slug="gallery", locale=default_locale).first()
Expand Down Expand Up @@ -108,6 +112,7 @@ def generate(seed):
search_description=fake.sentence(nb_words=10).rstrip("."),
hero_image=random.choice(images) if images else None,
hero_image_alt_text=fake.sentence(nb_words=8).rstrip("."),
expert=experts[i % len(experts)] if experts else None,
project_link=fake.url(),
body=[{"type": "rich_text", "value": body_html}],
)
Expand Down
11 changes: 11 additions & 0 deletions foundation_cms/static/js/components/gallery_hub/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ export const GALLERY_HUB_SELECTORS = {
projectMarker: "[data-gallery-hub-project-marker]",
modalLayer: "[data-gallery-hub-modal-layer]",
modal: "[data-gallery-hub-modal]",
modalScrollable: "[data-gallery-hub-modal-scrollable]",
modalToggle: "[data-gallery-hub-modal-toggle]",
modalClose: "[data-gallery-hub-modal-close]",
projectListItem: "[data-gallery-hub-project-list-item]",
projectListItemShell: "[data-gallery-hub-project-list-item-shell]",
projectListSlot: "[data-gallery-hub-project-list-slot]",
filterSlot: "[data-gallery-hub-filter-slot]",
};
Expand All @@ -38,6 +41,7 @@ export const GALLERY_HUB_CLASSES = {
introEntering: "gallery-hub--intro-entering",
mobileCompact: "gallery-hub--mobile-compact",
mobileShort: "gallery-hub--mobile-short",
modalClosing: "gallery-hub-modal--closing",
modalOpen: "gallery-hub--modal-open",
projectActive: "gallery-hub-project--active",
projectMarkerActive: "gallery-hub__project-marker--active",
Expand Down Expand Up @@ -105,6 +109,13 @@ export const GALLERY_HUB_VIEWPORT_OFFSET_PROPERTY =
*/
export const GALLERY_HUB_INTRO_ENTERING_DURATION = 1200;

/**
* Duration for modal exit animations before hidden state is applied.
*
* @type {number}
*/
export const GALLERY_HUB_MODAL_CLOSE_DURATION = 220;

/**
* Legacy deep link stripped on load so the JS-controlled experience starts at
* the top of the Gallery Hub.
Expand Down
241 changes: 235 additions & 6 deletions foundation_cms/static/js/components/gallery_hub/overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,55 @@
* @module galleryHubOverlay
*/

import { GALLERY_HUB_CLASSES, GALLERY_HUB_SELECTORS } from "./config";
import {
GALLERY_HUB_CLASSES,
GALLERY_HUB_MODAL_CLOSE_DURATION,
GALLERY_HUB_SELECTORS,
GALLERY_HUB_VIEW_MODES,
} from "./config";
import {
getGalleryHubState,
setGalleryHubState,
subscribeGalleryHubState,
} from "./state";

const FOCUSABLE_SELECTOR = [
"a[href]",
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
"[tabindex]:not([tabindex='-1'])",
].join(",");

let lastFocusedElement = null;
let closeModalTimer = null;

/**
* Return the currently open modal panel.
*
* @param {HTMLElement[]} modals - Modal panel elements.
* @param {?string} modalOpen - Currently open modal id.
* @returns {?HTMLElement} Open modal element, if any.
*/
function getOpenModal(modals, modalOpen) {
return (
modals.find((modal) => modal.dataset.galleryHubModal === modalOpen) || null
);
}

/**
* Return focusable controls in a modal, skipping hidden list rows.
*
* @param {HTMLElement} modal - Modal panel element.
* @returns {HTMLElement[]} Focusable controls.
*/
function getFocusableElements(modal) {
return Array.from(modal.querySelectorAll(FOCUSABLE_SELECTOR)).filter(
(element) => !element.closest("[hidden]"),
);
}

/**
* Reflect the current modal id onto the overlay layer, modal panels, and
* trigger aria attributes.
Expand All @@ -35,6 +77,10 @@ function syncModal(root, modalLayer, modals, toggles, modalOpen) {
const isOpen = modal.dataset.galleryHubModal === modalOpen;

modal.hidden = !isOpen;

if (isOpen) {
modal.classList.remove(GALLERY_HUB_CLASSES.modalClosing);
}
});

toggles.forEach((toggle) => {
Expand All @@ -44,6 +90,140 @@ function syncModal(root, modalLayer, modals, toggles, modalOpen) {
});
}

/**
* Close the active modal after its exit animation completes.
*
* @param {HTMLElement[]} modals - Modal panel elements.
*/
function closeOpenModal(modals) {
const state = getGalleryHubState();
const modal = getOpenModal(modals, state.modalOpen);

if (!modal) {
setGalleryHubState({ modalOpen: null });
return;
}

if (modal.classList.contains(GALLERY_HUB_CLASSES.modalClosing)) return;

if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setGalleryHubState({ modalOpen: null });
return;
}

window.clearTimeout(closeModalTimer);
modal.classList.add(GALLERY_HUB_CLASSES.modalClosing);

closeModalTimer = window.setTimeout(() => {
setGalleryHubState({ modalOpen: null });
closeModalTimer = null;
}, GALLERY_HUB_MODAL_CLOSE_DURATION);
}

/**
* Keep the project list rows aligned with the filtered carousel state.
*
* @param {HTMLElement[]} items - Project list buttons.
* @param {HTMLElement} root - Gallery Hub root element.
* @param {Object} state - Current Gallery Hub state snapshot.
*/
function syncProjectList(items, root, state) {
const visibleProjectIds = new Set(state.filteredProjectIds);
const activeProjectId = state.filteredProjectIds[state.activeIndex];
let visibleCount = 0;

items.forEach((item) => {
const projectId = item.dataset.projectId;
const isVisible = visibleProjectIds.has(projectId);
const isActive = projectId === activeProjectId;
const itemShell = item.closest(GALLERY_HUB_SELECTORS.projectListItemShell);

if (itemShell) itemShell.hidden = !isVisible;
item.setAttribute("aria-current", isActive ? "true" : "false");

if (isVisible) visibleCount += 1;
});

const empty = root.querySelector("[data-gallery-hub-project-list-empty]");

if (empty) empty.hidden = visibleCount > 0;
}

/**
* Move focus into a newly opened modal.
*
* @param {HTMLElement[]} modals - Modal panel elements.
* @param {?string} modalOpen - Currently open modal id.
* @param {Object} state - Current Gallery Hub state snapshot.
*/
function focusOpenModal(modals, modalOpen, state) {
const modal = getOpenModal(modals, modalOpen);

if (!modal) return;

const activeProjectId = state.filteredProjectIds[state.activeIndex];
const activeItem = modal.querySelector(
`${GALLERY_HUB_SELECTORS.projectListItem}[data-project-id="${activeProjectId}"]`,
);
const focusable = getFocusableElements(modal);

window.requestAnimationFrame(() => {
if (activeItem && !activeItem.closest("[hidden]")) {
activeItem.focus();
return;
}

focusable[0]?.focus();
});
}

/**
* Return focus to the modal trigger after the overlay closes.
*/
function restoreTriggerFocus() {
if (!lastFocusedElement?.isConnected) return;

lastFocusedElement.focus();
lastFocusedElement = null;
}

/**
* Keep Tab key focus inside the active modal.
*
* @param {KeyboardEvent} event - Keydown event.
* @param {HTMLElement[]} modals - Modal panel elements.
* @param {?string} modalOpen - Currently open modal id.
*/
function trapModalFocus(event, modals, modalOpen) {
if (event.key !== "Tab") return;

const modal = getOpenModal(modals, modalOpen);

if (!modal) return;

const focusable = getFocusableElements(modal);

if (!focusable.length) {
event.preventDefault();
modal.focus();
Comment thread
ramram-mf marked this conversation as resolved.
return;
}

const first = focusable[0];
const last = focusable[focusable.length - 1];

if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
return;
}

if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}

/**
* Initialize Gallery Hub modal overlay controls.
*/
Expand All @@ -57,34 +237,83 @@ export function initGalleryHubOverlay() {
const toggles = Array.from(
root.querySelectorAll(GALLERY_HUB_SELECTORS.modalToggle),
);
const projectListItems = Array.from(
root.querySelectorAll(GALLERY_HUB_SELECTORS.projectListItem),
);
let previousModalOpen = getGalleryHubState().modalOpen;

subscribeGalleryHubState((state) => {
const modalWasOpen = Boolean(previousModalOpen);
const modalIsOpen = Boolean(state.modalOpen);

syncProjectList(projectListItems, root, state);
syncModal(root, modalLayer, modals, toggles, state.modalOpen);

if (!modalWasOpen && modalIsOpen) {
focusOpenModal(modals, state.modalOpen, state);
}

if (modalWasOpen && !modalIsOpen) {
restoreTriggerFocus();
}

previousModalOpen = state.modalOpen;
});

syncProjectList(projectListItems, root, getGalleryHubState());
syncModal(root, modalLayer, modals, toggles, getGalleryHubState().modalOpen);

toggles.forEach((toggle) => {
toggle.addEventListener("click", () => {
const state = getGalleryHubState();
const modal = toggle.dataset.galleryHubModalToggle;

lastFocusedElement = toggle;

if (state.modalOpen === modal) {
closeOpenModal(modals);
return;
}

window.clearTimeout(closeModalTimer);

setGalleryHubState({
modalOpen: state.modalOpen === modal ? null : modal,
modalOpen: modal,
});
});
});

root.querySelectorAll(GALLERY_HUB_SELECTORS.modalClose).forEach((close) => {
close.addEventListener("click", () => {
setGalleryHubState({ modalOpen: null });
closeOpenModal(modals);
});
});

document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
if (!getGalleryHubState().modalOpen) return;
const state = getGalleryHubState();

setGalleryHubState({ modalOpen: null });
if (event.key === "Escape" && state.modalOpen) {
closeOpenModal(modals);
return;
}

trapModalFocus(event, modals, state.modalOpen);
});

projectListItems.forEach((item) => {
item.addEventListener("click", () => {
const state = getGalleryHubState();
const projectIndex = state.filteredProjectIds.indexOf(
item.dataset.projectId,
);

if (projectIndex === -1) return;

setGalleryHubState({
activeIndex: projectIndex,
viewMode: GALLERY_HUB_VIEW_MODES.project,
});
closeOpenModal(modals);
});
});
}
Loading
Loading