- Add repertoire-scroll-restore.js: snapshots scrollTop of each column <ul>
before htmx:beforeSwap, restores after htmx:afterSwap (keyed by data-col)
- Add subtle opacity transition on #repertoire-index during htmx-swapping
- Tighten rep-indicator opacity transition to 0.1s for snappier feedback
- Import new module in public-entry.js
- repertoire.css: add min-height: 0 to column <ul> scroll containers so
grid 1fr row shrinks below content and overflow-y: auto activates
- admin-toc.js: add __adminTocBuilt guard + nav.children check to prevent
double population when loaded both via admin.min.js and direct <script>
- admin-toc.php: remove duplicate <script src="admin-toc.js"> tag —
JS is already bundled in admin.min.js
- just dev now spawns a background chokidar watcher alongside the PHP server
- CSS/JS changes auto-rebuild into dist/ (~200ms per rebuild)
- just stop kills both the PHP server and the watcher
- just dev-watch still available standalone for split-terminal workflows
- chokidar-cli added as devDependency
When filters are active, viable (matched) keywords now appear at the top
of the Mots-clés column so users don't have to scroll past faded entries
to find clickable tags. Both groups maintain alphabetical order internally.
The sticky chip bar added noise: users already see their active
filters highlighted in the accordion columns (with rep-entry--selected
styling and the active-count badge on each toggle). Removing chips
shifts focus back to the accordions, consistent with the expectation
that users return to the dropdowns to adjust filters.
The htmx:afterSwap handler was calling initAccordions(e.detail.target)
but after an outerHTML swap, e.detail.target references the old detached
DOM element, not the new live element. This meant accordion click
handlers were never attached to the new filter column toggles after
applying or removing a filter — breaking all subsequent interaction.
Fix: query the live DOM with document.querySelector() instead.
- repertoire-index.php: wrap each filter column in rep-accordion with toggle
button, chevron, badge (active filter count); add rep-chip-bar with
removable active-filter chips above the columns
- repertoire.css: mobile (≤640px) accordion mode — columns collapse to
single-open accordion sections with 48px touch targets; chip bar becomes
sticky; desktop/tablet layout unchanged via display:none on toggle elements
- repertoire.php: JS for single-accordion-open behavior on mobile, HTMX
re-init after swap, resize-breakpoint cleanup
- docs/repertoire-mobile-propositions.md: analysis + 4 architecture proposals
- Convert .apropos-toc <nav> to <details class="toc" open> in all three templates
- Add caret-down icon to summary (visible only on mobile via media queries)
- Desktop (≥768px): sticky sidebar via CSS grid, force-open via pointer-events:none, hide caret
- Mobile (≤767px): single column, collapsible TOC with rotating caret, margin-top below search bar
- Rename .apropos-toc-link to .toc-sidebar-link
- Merge 900px and 600px breakpoints into single 600px mobile media query
- Rename apropos.css → content-page.css; update 3 controller references
- Remove duplicate 'Téléversements abandonnés' heading already communicated by the summary
- Replace #peertube-orphans-wrapper with display:contents wrapper so PeerTube details sit as direct grid siblings
- Add hx-confirm to all delete buttons (filepond, trash, PeerTube orphans)
Replace every <img src="/assets/icons/..."> with <?= icon('name') ?>
across 26 template files. The PHP helper inlines the SVG markup into the
DOM so CSS color cascades naturally through fill="currentColor".
- Add src/icon.php helper: reads SVG file, sets width/height to 1em,
injects aria-hidden, supports optional CSS class
- Fix 12 icon SVGs that had hardcoded fill="#000000" or missing fill attr
- Replace search.svg with Phosphor fill-based magnifying glass
- Add explicit SVG sizes for admin header nav icons (16px/20px)
- Scope public search icon CSS to form[role=search]:not(.header-search-form)
to avoid breaking admin header layout; change stroke to fill
- Remove <img> filter: brightness(0) invert(1) hacks from admin.css
Unify the three public pages (à propos, charte, licence) onto a single
grid layout (.page-content) with sticky TOC sidebar, replacing the old
separate / / markup.
- Merge about.php, charte.php, licence.php templates into shared
.page-content / .content-section structure
- Add CommonMark HeadingPermalinkExtension for stable heading anchors
- Use SlugNormalizer for TOC links so they match rendered heading IDs
- Standardize link styling across content blocks: bold black, accent on
hover (consistent with global link style)
- Fix code block wrapping: use pre-wrap instead of pre, constrain grid
columns with min-width:0, auto scrollbar
- Fix apropos page grid placement: force content-section into column 2
so contacts and credits stay in the content area, not the sidebar
Also includes accumulated WIP changes:
- Header gradient: hardcoded purple-to-green (replaces CSS variables)
- Search placeholder font
- Duration field: replace minutes/sec/heures with h:m:s time inputs
- TFE file optional for formats 1,4,6 with client-side JS toggle
- Licence form: em-dash to hyphen, details/summary classes
- Pill search: block Enter key form submission when no results
- Draft autosave: remove CSRF rotation (broke concurrent FilePond uploads)
- Language pill: clear hints for excluded main languages
- Search results: gradient placeholder cards for items without covers
- TFE display: format durée values as XhYm instead of decimal
The just dev command hardcodes upload_max_filesize=512M and
post_max_size=520M via -d flags, which override .user.ini.
Raised to 8192M/8704M to match the JS-side 8GB video size
caps. Also raised max_execution_time and max_input_time to
600s to accommodate large file transfers and PeerTube uploads.
The htmx:beforeSend listener in admin/footer.php was a debugging
leftover that called new FormData(e.target.closest('fieldset')).
FormData only accepts HTMLFormElement or nothing — passing a
<fieldset> throws 'Argument 1 does not implement interface
HTMLFormElement'. Removed the serialization call; kept the
minimal debug log.
Raised upload_max_filesize from 512M to 8192M (8G) and post_max_size
from 520M to 8704M to match the JS-side per-extension size caps that
allow up to 8GB for video files (mp4, webm, mov, etc.). Also raised
memory_limit to 512M and max_input_time to 600s.
The closure returned arrays when formData values were arrays (e.g.
jury_promoteur), but the PHP return type annotation was :string.
PHP 8.x enforces this strictly, causing a fatal TypeError in
jury-fieldset.php on add mode.
Also introduces $extraCssAdmin support in head.php for admin-only
stylesheets (form-admin.css, filepond CSS, system.css). Admin pages
now use $extraCssAdmin for admin-only assets and $extraCss for
shared stylesheets like form-base.css.
- New fragment endpoint POST/GET /partage/fragments/draft.php:
saves all form fields to PHP session, excludes file/csrf/slug fields
GET returns JSON for JS hydration on page load
rotates both global CSRF and share CSRF tokens in sync
- form.php accepts optional $formExtraAttrs and $showAutosaveStatus:
allows injecting HTMX attributes and 'Brouillon enregistré' indicator
- renderShareLinkForm adds hx-post with change/input debounce trigger,
loads autosave-handler.js, hydrate fields from draft on page load
- Draft cleared on successful form submission in handleShareLinkSubmission
- autosave-handler.js now also updates share_link_token hidden input
when rotating CSRF token (partage form uses both csrf_token and share_link_token)
- Added .autosave-status CSS to form.css (was admin.css-only)
- Updated fragment routing to accept GET requests (needed for draft hydration)
The partage/admin form had a hardcoded filepond_mode=1 hidden input,
so without JavaScript the server always entered the FilePond async
path — which found no hex IDs and silently dropped all files.
Three-layer fix:
1. HTML: filepond_mode input starts disabled with value=0; JS enables
it and sets value=1 on DOMContentLoaded (and after HTMX swaps).
Disabled inputs aren't submitted → server gets no filepond_mode
→ naturally falls to legacy path.
2. JS: enableFilepondMode() called on page load and hx:afterSwap so
FilePond-enhanced forms always send filepond_mode=1.
3. Server (defense-in-depth): ThesisFileHandler::hasFilePondQueueData()
scans POST['queue_file'] for 32-char hex IDs; ThesisCreateController
and ThesisEditController use it alongside filepond_mode, so even if
the flag somehow arrives without async upload IDs, the path
takes over.
Add @media (max-width: 600px) rule to form.css:
- Stack form row labels above inputs (1fr grid, single column)
- Ensure 44×44px minimum touch targets on checkboxes, radios,
selects, textareas, text inputs, and .btn/.btn--sm
- Stack thesis-add-header and recap-dl grids to single column
- Stack form footer buttons vertically with full width
- Unstick sticky formats fieldset on mobile
- Tighten fieldset margins for narrow viewports
WCAG 3.3.1 (Error Identification): failing fields now get
aria-errormessage pointing to the flash-error container and
aria-invalid="true". WCAG 3.3.3 (Error Suggestion): <small>
hint text on inputs, selects, and file fields is now linked via
aria-describedby (always, not just on error).
Changes:
- text-field.php, select-field.php, checkbox-list.php: accept
$errorFieldName; add aria-errormessage/aria-invalid on match;
add id to <small> and aria-describedby on the control
- fieldset-tfe-info.php: aria-invalid on synopsis textarea
- fichiers-fragment.php: aria-describedby on cover, note
d'intention, TFE, annexes, and website inputs; aria-invalid
on format checkboxes when error matches 'formats'
- form.php: id="flash-error" + tabindex="-1" on flash-error
div; accept $errorFieldName from callers
- admin/add.php: set $errorFieldName, wire $withAutofocusFn
(was identity default)
- admin/edit.php: set $errorFieldName
- partage/index.php: consume autofocus field, wire autofocus
function, add App::flashAutofocus() in submit catch block
Also fixes WCAG standards issue: removed invalid 'required'
HTML attribute from <fieldset> elements in checkbox-list.php
and fichiers-fragment.php (only aria-required stays). Added
role="group" for explicit ARIA semantics.
ro=['fire','_read','_write'] is an exclusion list in Ee(), not an inclusion
list. The external pond object has none of these. The only safe interception
point is inside the closure (vendor patch), but the root-cause fix
(fileValidateSizeFilter .filename → .name) already prevents the crash.
Investigation verdict:
- HTMX does NOT swap the FilePond container on the edit page; the
htmx:targetError in the crash log is unrelated noise
- The pre-destroy abort in destroyFilePondsIn has a wrong status check
(filters for nonexistent status 4, misses status 7 LOADING) but is
moot because no HTMX swap targets the FilePond container
- The load-file-error -> DID_THROW_ITEM_INVALID path is vulnerable
(passes t.status directly, unlike every other error handler which
wraps it), but for local files the LOAD_FILE plugins always wrap
rejections properly
- Likely actual trigger: Firefox XHR abort edge case in server.load
for the existing cover file, racing with addition of a new local file
that replaces it in the single-file queue
Documents the 'can't access property main, n.status is undefined'
crash in FilePond 4.32.12. Root cause: vendor code in filepond.min.js
has a property name mismatch — createResponse objects use .code but the
load-file-error handler reads .status. When action.status is undefined,
the view writers crash.
Proposes Option B (custom load function) as the cleanest fix.
Fixes three root causes of FilePond errors on TFE upload forms:
1. server.process.onerror accessed .status on a string (XHR response
text body) — now extracts the body safely.
2. server.load was a bare URL string with no error handling — converted
to object with onload/onerror to prevent FilePond internal _write
crash when load.php returns HTTP errors.
3. destroyFilePondsIn now aborts in-flight processing before pond.destroy()
to prevent stale XHR callbacks firing on a torn-down FilePond instance.
Server-side: FilepondHandler now emits Content-Type: text/plain on all
responses (PHP defaults to text/html on die(), confusing FilePond's
response parser).
Replace text labels (h1, bold, italic) with rendered HTML in the Rendu column:
headings, strong, em, del, code, links, blockquote, lists, hr, sup, small
validateAndSanitise() no longer cross-contaminates:
- contact_interne overwrote mail, which then copied to contact_visible
- Fixed: contactInterne from contact_interne (admin) or confirmation_email (student)
- Fixed: contactVisible from contact_visible (admin) or mail (student)
- Fixed: submit() uses contactInterne as author email, not mail
1. maxFileSize bug: FileValidateSize plugin overrides core's maxFileSize
setter. Core uses toBytes('1GB') = 1073741824, but plugin registers
maxFileSize as [null, Type.INT] which calls toInt('1GB') = 1.
Fix: all maxFileSize and perExtensionMaxSize values as raw bytes.
Also fix option name: fileValidateSizeFilterItem → fileValidateSizeFilter.
2. Temp file persistence: files uploaded via FilePond went to
tmp/filepond/ and vanished from the UI on page reload because
data-existing-files only included DB-persisted files.
Fix: session-track temp file_ids in handleProcess, inject via
getSessionTempFiles() into data-existing-files, teach handleLoad
to stream temp files from disk, and route JS remove → revert for hex IDs.