Add DA folder picker and per-item image upload to Homepage configurator - #244
Merged
Conversation
Introduces a parallel, opt-in layout system for event pages: setting page metadata `section-layout: columns` makes the page's <main> a flex row of its top-level .section children (columns), each still stacking blocks normally inside. Below 900px, columns collapse back to normal vertical stacking. Per-column width ratios are authored via the existing Section Metadata block's generic `style` key (column-span-1/2/3), reusing Milo's existing class-attachment mechanism with zero changes to Milo. The new applySectionColumnsLayout helper is idempotent and always resolves the real page <main>, since decorateEvent re-enters for fragments/personalization/events-form with non-<main> parents. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
applySectionColumnsLayout now toggles the class based on a fresh getMetadata() read instead of a one-time "checked" flag, so a personalization pass that updates section-layout metadata after the first decorateEvent call still takes effect. Also drops the redundant column-span-1 CSS rule, which just duplicated the existing default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
decorateEvent only runs on pages with event-id, so its internal call to applySectionColumnsLayout() never reached non-event pages. Consuming sites (e.g. da-events) need to call it directly and unconditionally from their own decorateArea instead. Export it (and no longer call it from decorateEvent) via the libs.js barrel, and make it load its own required CSS internally so callers only need this one function instead of having to remember to pair it with addStylesToEventPage(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the "all sections become equal columns" behavior with selective per-row grouping: sections default to full width (flex: 1 1 100%, forcing each onto its own line, identical to normal stacking). Only sections tagged column-span-1/2/3 shrink from 100% and pack onto a shared line with their contiguous column-span-tagged neighbors. An untagged section can never fit alongside anything else, so it always forces a line break before and after itself — grouping is inherently contiguous by construction, with no validation logic needed. Uses flex-wrap instead of CSS Grid specifically because flexbox recalculates fill-percentage per line independently, so a group's flex-grow ratios always sum to fill their shared row completely regardless of how many items are in it — a fixed-track grid would leave a visible gap when a group's spans don't sum to the template's column count. No JS or test changes: applySectionColumnsLayout() only ever toggled a class and ensured the stylesheet loads; all grouping logic now lives entirely in this CSS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…match Split the column-span-1/2/3 rules' min-width declaration onto its own selector block (declaration-block-single-line-max-declarations was being violated by packing flex + min-width onto one line). Also normalize the section-layout metadata comparison with .trim().toLowerCase(), matching how other author-facing metadata values are already handled elsewhere in this file (e.g. the theme/ style comparisons) — content authors type this value directly, so it shouldn't silently no-op on stray capitalization or whitespace. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds docs/section-columns-layout.md following this repo's existing authoring-guidelines doc style (see track-icon-config-authoring- guidelines.md): the metadata contract, how to group sections into a row via column-span-1/2/3, why grouping is inherently contiguous, and the technical rationale for the no-DOM-reparenting, flex-wrap-based design. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flexbox (and CSS Grid) default align-items to stretch, which was making every section in a shared row match the height of its tallest sibling. Set align-items: flex-start on main.section-columns so each section keeps sizing to its own content, the same way it does as a normal direct child of <main> outside this feature. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Revert to align-items: stretch on main.section-columns. This equalizes section height within a shared row (useful for backgrounds/borders lining up cleanly) without stretching content inside, since .section lays out its children as normal block flow rather than flex/grid. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… rule Adds to the authoring guide: the general "own weight / row's total weight" ratio formula (with the 60/40 and thirds examples), that any number of contiguous tagged sections can share a row (not just two), and that two distinct back-to-back groups need an untagged divider section between them since only a full-width section forces a line break. Includes a second worked example with multiple independent rows, a solo full-width section, and a 3-up row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SESSION_CODE_PATTERN only matched an "S" prefix (e.g. s6304), so session codes with other RainFocus prefixes (OS565, AL687, ...) fell through to the metadata-key branch instead, overwriting the real featured-sessions key and silently failing to hydrate that card's tokens. Broadens the pattern to any 1-4 letter prefix + digits, and adds a regression test for an OS-prefixed code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SESSION_CODE_PATTERN only matched an "S" prefix (e.g. s6304), so session codes with other RainFocus prefixes (OS565, AL687, ...) fell through to the metadata-key branch instead, overwriting the real featured-sessions key and silently failing to hydrate that card's tokens. Broadens the pattern to any 1-4 letter prefix + digits, and adds a regression test for an OS-prefixed code. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Since a card already authors its own metadata key as a class (e.g. featured-sessions), repeating that key in every token was redundant. rewriteToken() now also accepts a bare, prefix-less token — [[enTitle]] rewrites to the same [[featured-sessions:3. enTitle]] as [[featured-sessions.enTitle]] would. Both forms can be mixed on the same card; a dotted token starting with a different key is left untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[MWPW-201134] Support bare-token shorthand in card-c2 hydrator
Helix cache purges must target exact file paths (no wildcards), so PRs into main now get a bot comment listing changed non-doc files prefixed with a `<domain>` placeholder for quick find-and-replace. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Clean rename with no back-compat bridge (nothing published yet): - event-libs/v1/c2/blocks/card-c2/ -> event-card/ (card-c2.js/.css -> event-card.js/.css; session-routing.js's .card-c2[data-mr-stream-id] selector updated) - event-libs/v1/c2/blocks/slider/ -> event-carousel/ (slider.js/.css -> event-carousel.js/.css; its own .card-c2 track-matching selectors updated to .event-card) - event-libs/v1/hydrate/card-c2.js -> event-card.js; hydrate.js's HYDRATORS registry key and import updated; hydrateCardC2 renamed to hydrateEventCard - libs.js's EVENT_BLOCKS_C2 entries updated - All tests, mocks, and docs (event-card/docs/*, event-carousel/docs/ README.md, upcoming-sessions/docs/README.md) renamed/updated to match Authors must now use event-card/event-carousel as the block name in DA; the old card-c2/slider class names no longer resolve to anything. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consistent with the card-c2/slider -> event-card/event-carousel rename in this PR. dark-card is still deliberately not named "dark" (Milo's own reserved global dark-background class), same reasoning as before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename card-c2 block to event-card, slider block to event-carousel
Add PR workflow to list changed code files for cache purging
[MWPW-201133]New event marquee
Add opt-in flex-column section layout
Authors can now attach an image to each Upcoming/Featured Sessions item: pick or create a DA folder via a new column browser, upload a local image into it, and the resulting content.da.live URL is written to the session's imageUrl meta field and included in the copied JSON output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…edupe auth module - Fold da-media-controller.js's listFolder/uploadMedia into da-sheet-controller.js so every DA admin-API call shares one auth singleton instead of two independently-synced copies. - Reject '.'/'..' path segments (folder-name input, uploaded filename) before they reach admin.da.live — URL dot-segment normalization could otherwise retarget a request outside the intended org/repo. - encodeURIComponent the uploaded filename before splicing it into the request and content.da.live URLs. - Guard ImagePickerModal's folder loads against out-of-order responses and clean up stale columns on a failed fetch. - Fix the "Add folder" loading guard (was comparing against the wrong column index and never actually engaged). - Block every modal-close path while an upload is in flight, so closing mid- upload can no longer force-close a picker reopened for a different session. - Minor: drop a redundant root refetch, align a network-error message. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch and validate page speed.
Commits
|
…ased output Author feedback on the first pass (folder picker + raw content.da.live URL + JSON copy) drove four changes: - Drop the DA folder-picker UI entirely. Every upload now lands in one fixed, dot-prefixed hidden folder the app owns, namespaced by event + timestamp — authors never choose or see the destination. - content.da.live isn't a servable site asset. Uploads are now previewed and published via the Helix/AEM admin API (admin.hlx.page), and the resulting aem.live URL is what lands in imageUrl. - Image field now renders on its own line instead of squeezed into the compact watchUrl/mrStreamId row. - Replace "Copy Homepage JSON" with "Copy Link", mirroring Schedule Maker: the full session-entry payload is base64-encoded into the app's own URL hash, copied as a real hyperlink (text prefixed with the config type and timestamped), and clicking it deep-links straight back into that config's editor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tools/da-apps/tier-1-event-configurator is just where this app's own loader HTML lives — it isn't part of the site's Helix content mount, so uploaded assets there never resolved to a servable preview/publish URL. events/homepage-assets is under the site's actual published content tree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root-caused against real request/response evidence: the multipart body sent to admin.da.live was empty (a raw File passed to FormData.append loses its bytes through the DA SDK's wrapped fetch — writeSheet never hit this because its own body is always an already-materialized Blob), and DA lowercases the filename segment on write, so previewAsset/ publishAsset were targeting a path that didn't match where the file actually landed. - uploadMedia now reads the File into a real Blob before appending it. - uploadMedia parses DA's response body and returns its canonical stored path/URLs instead of assuming the request path was honored. - uploadAndPublishMedia previews/publishes that canonical path, and prefers DA's own returned liveUrl over re-deriving one. - buildMediaAssetPath lowercases up front too, so the path we log/assume matches what DA will actually store from the start. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
accept="image/jpeg" is just a picker hint, not enforcement, so handleFileChange also validates the actual selected file (falling back to extension when a file has no MIME type) and rejects anything else before it's ever eligible for upload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The link is authored directly in the block's doc body now, not a section-metadata row — drop the stale metadataKey field and update the toast/hint copy to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nt-card
upcoming-sessions.js and a new featured-sessions block now consume the
Homepage configurator's link directly: decorate.js's tec-homepage auto-block
builder decodes the link's hash payload and builds the right block with
{ heading, entries } as a data attribute, replacing both the old JSON-in-
section-metadata authoring path and the hydrate/featured-sessions/session-code
classname mechanism. Featured Sessions cards are generated by reusing
event-card.js's own init() (still a generic, independently-used block) rather
than duplicating its media/body/session-routing logic. Images are now
auto-populated from the link payload on both surfaces; an entry with no image
is dropped by event-card.js's existing "no image, no card" rule. Adds a
"Section heading" field to the configurator UI, threaded through the link
payload since there's no authored row left to source it from.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review findings from the previous commit's pivot: - featured-sessions.js and upcoming-sessions.js built card titles/tracks/ heading via createTag's string html argument (insertAdjacentHTML), which parses markup rather than treating it as text. That data now comes from a decoded link payload rather than hand-authored DOM/section-metadata, so an attacker-influenced value there would execute. Switched those call sites to set .textContent after creating the element. - featured-sessions.js set the CTA href straight from the decoded entry's url with no scheme check; a card missing sessionId never gets session-routing.js's click interception (and its safeUrl re-check), so a javascript: URI would fire on a plain click. Now sanitized via the existing safeUrl() allowlist before ever becoming an href. - Deduped the eventId-mismatch log guard that was copy-pasted between the sessions-guide and tec-homepage auto-block builders in decorate.js into one warnIfEventIdMismatch() helper. - Fixed a wrong relative-import depth in featured-sessions.test.js that was silently failing to load the module under test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…icker for image uploads Visual refresh: --tec-* custom properties in tier-1-event-configurator.css now resolve to --s2a-* Spectrum S2 design tokens (imported from v1/c2/styles/tokens.css) instead of hardcoded hex, matching the tokens already established elsewhere in this repo (sessions-guide, c2 blocks, the shared toast). Also: removed the 960px max-width cap on .tec-content (fluid width), buttons are now fully pill-shaped, the "Section heading" input is wider/more prominent, the editor back-button is vertically centered against its two-line title block, the toast dropped its non-Spectrum left-border stripe for solid variant backgrounds, and the default browser 8px body margin is reset to 0. Restored the DA folder-browser UI in ImagePickerModal.js (column browser, breadcrumbs, "Add folder" pseudo-create-on-first-upload) that an earlier commit on this branch had pivoted away from in favor of one fixed, hidden upload folder. Kept everything from that later pivot that wasn't about removing author choice: uploads still go through uploadAndPublishMedia (upload, then preview, then publish, so the field ends up with a real live aem.live URL) and JPG-only validation. utils.js's buildMediaAssetPath (always-fixed path) is replaced by getDefaultMediaFolder(eventId) + buildMediaFileName(fileName) — the picker defaults selectedFolderPath to the event's own conventional folder, but authors can browse elsewhere or type a new folder name to override it. Security fix (from independent review): hasUnsafePathSegment in v1/utils/da-sheet-controller.js only rejected literal '.'/'..' path segments, which a percent-encoded equivalent (e.g. %2e%2e) could bypass since neither listFolder nor uploadMedia decode the path before use. It now decodes each segment (treating a malformed/undecodable segment as unsafe) before comparing. The new folder-name input is now also restricted client-side to [A-Za-z0-9._-], matching the character allowlist already used elsewhere in this file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
featured-sessions.js was built as a plain wrapping grid (flex-wrap, no controls) while its sibling upcoming-sessions.js — built in the same commit, from the same tec-homepage auto-block builder — is a real horizontally-scrolling carousel with arrow controls. That divergence wasn't a deliberate design choice; every "Featured Sessions"-style section on the team's own cards.plain.html demo doc renders as a carousel, confirming this block should match. Rather than duplicating event-carousel.js's arrow/scroll-state logic a third time (event-carousel.js and upcoming-sessions.js already each have their own copy), featured-sessions.js now reuses event-carousel.js directly: it builds a .carousel-track holding the cards plus a bare .event-carousel marker element, places them as siblings, and calls event-carousel.js's own init() on the marker — the same selfInit reasoning mobile-rider.js already uses inside event-marquee.js. Since that bypasses Milo's normal per-block CSS auto-load (the marker is built programmatically, not scanned from authored content), event-carousel.js's init() now loads its own stylesheet first, matching mobile-rider.js's existing precedent. The heading is deliberately not routed through event-carousel.js's own heading mechanism: that lives inside .carousel-controls, which is hidden below 900px by design — fine for optional arrows, wrong for the block's actual heading text (real page content, not a random encoding bug: it's attacker-influenced, decoded from the link's hash payload, so it's set via .textContent rather than createTag's HTML-argument path). The heading instead gets its own always-visible element, and the now-decorated marker is moved next to it afterward purely for layout — safe once event-carousel.js's click handlers/ResizeObserver are already bound to real elements, not DOM position. Also removed hardcoded font-family declarations from event-card.css and event-carousel.css's .carousel-pill so the site's global font rules cascade through instead of being pinned locally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ence Saving a config redirected back to the library list immediately afterward — present since the app's very first commit, not something recent work introduced, but not useful now that saving gives its own toast confirmation (ConfigsContext's saveActiveConfig already sets toastSuccess). Authors editing a config likely want to keep working on it after a save, not get bounced back to the list. Cancel still returns to the library, since discarding and leaving is exactly what that button means. MOBILE-RIDER-STREAM-ID-GAP.md described buildSessionAuthorEntry() as building JSON pasted into section-metadata and listed a manual mrStreamId/watchUrl input field as an undecided option — both stale: the pre-link section-metadata authoring path no longer exists (superseded by Copy Link), and that manual field was already implemented in FeaturedSessionsEditor.js. Updated the doc to describe the actual current flow and mark the decision resolved. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sharmeebuilds
approved these changes
Aug 14, 2026
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an end-to-end image + link-based authoring flow for Homepage session
surfaces (Upcoming Sessions and Featured Sessions) in the Tier 1 Event
Configurator, wires up the actual rendering blocks to consume it, and
refreshes the configurator's own UI to align with Spectrum S2.
Authoring (Tier 1 Event Configurator, Homepage editor)
(column browser + breadcrumbs; "Add folder" is purely client-side —
DA has no folder-creation endpoint, so a typed name only becomes a real
folder once the first upload lands in it), then pick a JPG and upload.
Defaults to the event's own conventional folder
(
events/homepage-assets/<eventId>) so doing nothing still landssomewhere organized, but authors can redirect elsewhere. Uploads are
previewed and published via the Helix/AEM admin API, so the field ends
up with a real, live
.aem.liveURL, not a rawcontent.da.livelink."Copy JSON" output: a single link (mirroring Schedule Maker's pattern)
encodes the full session list — including each session's image, watch
URL, and Mobile Rider stream ID — as a base64 payload in the link's URL
hash, prefixed with the config type and a timestamp in its link text.
Authors paste that link directly into the target page's doc body.
Clicking the link also reopens the configurator on that exact config.
--tec-*custom properties nowresolve to
--s2a-*design tokens instead of hardcoded hex, matchingtokens already established elsewhere in this repo. Also: no more 960px
content-width cap, fully pill-shaped buttons, a wider/more prominent
"Section heading" input, a vertically-centered editor back-button, and a
toast redesigned to drop its non-Spectrum left-border stripe in favor of
solid variant backgrounds.
Rendering (event-libs blocks)
decorate.jsgained atec-homepageauto-block builder (alongside theexisting
chrono-box/sessions-guideones): it finds the pasted link,decodes its payload, and replaces it with either an
upcoming-sessionsor a
featured-sessionsblock, carrying the decoded config as a dataattribute.
upcoming-sessions.jsreads that config directly and renders eachsession's image with its own carousel controls.
featured-sessionsbuilds cards fully from the payload, reusing theexisting generic
event-cardblock's own media/body-building andsession-routing logic rather than duplicating it — and now renders as a
real horizontally-scrolling carousel (matching every "Featured
Sessions"-style section elsewhere on the team's own demo doc) by reusing
event-carousel.js's track/arrow logic directly, instead of the plainwrapping grid (no controls) it originally shipped as. This retires the
old
hydrate/featured-sessions-classname/session-code mechanism(
hydrate/event-card.js, deleted), which required one hand-placedevent-cardper session and never supported images at all.Review notes
Went through several rounds of independent review (correctness, security,
simplicity) across this branch's history. Fixes from the latest round:
hasUnsafePathSegment's path-traversal guard onlyrejected literal
./..segments; a percent-encoded equivalent(
%2e%2e) could bypass it since neitherlistFoldernoruploadMediadecode the path before use. It now decodes each segment first (treating
an undecodable segment as unsafe). The restored folder-name input is
also now restricted client-side to
[A-Za-z0-9._-].font-familyso global rules cascadethrough was correct for
<p>/<a>elements (which inherit normally),but broke
.carousel-pill, a<button>— buttons don't inheritfont-familyfrom ancestors by browser default. Fixed withfont-family: inherit(not a hardcoded name).paddinghad been added to.featured-sessionsalongside itsposition: relativestacking-contextfix, violating this repo's own documented rule against hardcoding block
spacing without justification (MWPW-201396). Removed, keeping only the
positioning fix.
featured-sessionsheading) are set via
.textContentrather thancreateTag'sinsertAdjacentHTML-backedhtmlargument, since that data comes from adecoded link payload rather than hand-authored DA content.
hrefis sanitized through the existingsafeUrl()allowlist before being set.
Noted as follow-ups, not addressed in this PR (real but out of scope
for a clean, bounded change):
ImagePickerModal.js's restored folder browser substantially overlapsschedule-maker/components/editor/FragmentPathBrowser.js(same stateshape, identical helper functions). Not a clean drop-in extraction —
schedule-maker's version is file-selection-oriented with deep-link
auto-expansion and no folder-creation, this one is folder-only with
pseudo-create — but a shared, parameterized component is worth designing
separately.
featured-sessions.jsreusesevent-carousel.jsvia a synthetic emptymarker element routed through its full
init()pipeline, rather thanevent-carousel.jsexporting the 2-3 helpers actually needed (e.g.buildArrows). Works correctly as-is; the more direct alternative wouldchange
event-carousel.js's export surface, which the manually-authoredblock flow also depends on.
Test plan
npm run lintnpm test(full suite passing, 1465 tests)?ref=localDA proxy flow, sign inat da.live
session via the restored folder picker — browse an existing folder,
create a new one, confirm it lands in DA, previews, and publishes;
set a section heading; Copy Link
that the full card list renders with no per-card authoring, the
heading is visible (not hidden below 900px), each session's image
shows up, and Featured Sessions now scrolls as a carousel with
working arrows
configurator on that exact config
🤖 Generated with Claude Code