From 19bf9f101ac6ab3eb0312fcc67495a6d218798ba Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Mon, 15 Jun 2026 16:35:17 +0200 Subject: [PATCH] Refactor apropos/charte/licence pages: shared layout, TOC anchors, and UI polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .phpunit.result.cache | 2 +- TODO.md | 22 +- app/public/admin/actions/draft.php | 14 +- app/public/assets/css/apropos.css | 232 ++++++++++-------- app/public/assets/css/colors.css | 8 +- app/public/assets/css/components/header.css | 27 +- app/public/assets/css/components/search.css | 4 +- app/public/assets/css/form-base.css | 28 +++ app/public/assets/css/public.css | 4 +- app/public/assets/css/repertoire.css | 54 +++- .../assets/js/app/file-upload-filepond.js | 63 +++++ app/public/assets/js/app/pill-search.js | 8 +- app/public/partage/fragments/draft.php | 13 +- app/public/partage/pill-search-fragment.php | 10 +- app/src/Controllers/CharteController.php | 11 +- app/src/Controllers/LicenceController.php | 11 +- .../Controllers/ThesisCreateController.php | 2 +- app/src/Form/FormBootstrap.php | 8 +- app/src/MarkdownHelper.php | 11 +- .../partials/form/fichiers-fragment.php | 4 +- .../form/fieldset-licence-explanation.php | 42 ++-- app/templates/partials/form/form.php | 108 ++++++-- app/templates/public/about.php | 166 ++++++------- app/templates/public/charte.php | 45 ++-- app/templates/public/licence.php | 45 ++-- app/templates/public/search.php | 9 +- app/templates/public/tfe.php | 27 +- 27 files changed, 636 insertions(+), 342 deletions(-) diff --git a/.phpunit.result.cache b/.phpunit.result.cache index b5a34fb..fccf374 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0.002,"CryptoTest::testEncryptDecryptWithUnicode":0,"CryptoTest::testEncryptDecryptMultiline":0,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0.001,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0.001,"CryptoTest::testIsEncryptedRejectsPlaintext":0,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0.001,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0.001,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0.001,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0.001,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0.001,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0.001,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0.001,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0.002,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0.001,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0.001,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0.001,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0.001,"EmailObfuscatorTest::testMultipleEmailsInOneString":0.001,"EmailObfuscatorTest::testEmailWithPlusSign":0.001,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0.001,"StudentEmailTest::testBuildHtmlContainsKeyFields":0.001,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0.001,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0.001,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0.001,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0.001,"StudentEmailTest::testBuildHtmlContainsLabelFields":0.001,"SystemControllerHelpersTest::testHumanBytesZero":0.001,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0.001,"SystemControllerHelpersTest::testHumanBytesOneKB":0,"SystemControllerHelpersTest::testHumanBytesOneMB":0,"SystemControllerHelpersTest::testHumanBytesOneGB":0,"SystemControllerHelpersTest::testHumanBytes1523MB":0,"SystemControllerHelpersTest::testHumanBytes2500GB":0.001,"SystemControllerHelpersTest::testDiskColorBelowWarning":0.001,"SystemControllerHelpersTest::testDiskColorWarning":0.001,"SystemControllerHelpersTest::testDiskColorCritical":0.001,"SystemControllerHelpersTest::testLogLineClassCrit":0.001,"SystemControllerHelpersTest::testLogLineClassError":0.001,"SystemControllerHelpersTest::testLogLineClassWarn":0,"SystemControllerHelpersTest::testLogLineClassNotice":0.001,"SystemControllerHelpersTest::testLogLineClassHttp500":0.001,"SystemControllerHelpersTest::testLogLineClassHttp300":0.001,"SystemControllerHelpersTest::testLogLineClassDefault":0.001,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemControllerHelpersTest::testStatusLabelActive":0,"SystemControllerHelpersTest::testStatusLabelInactive":0.001,"SystemControllerHelpersTest::testStatusLabelFailed":0,"SystemControllerHelpersTest::testStatusLabelWarn":0,"SystemControllerHelpersTest::testStatusLabelUnknown":0,"SystemControllerHelpersTest::testStatusClassOk":0,"SystemControllerHelpersTest::testStatusClassWarn":0.001,"SystemControllerHelpersTest::testStatusClassError":0.001,"SystemControllerHelpersTest::testStatusClassUnknown":0.001,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0.001,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0.001,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0.001,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0.001,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0.001,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0.001,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0.001,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0.001,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0.001,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0.001,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0.001,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0.001,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0.001,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0.001,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0.001,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0.001,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0.001,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0.001,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0.001,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0.002,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0.001,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0.001,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0.001,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0.001,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0.001,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0.001,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0.001,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0.001,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0.001,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0.002,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.008,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0.001,"DatabaseExtendedTest::testRenameTagUpdatesName":0,"DatabaseExtendedTest::testMergeTagReassignsTheses":0.001,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0.002,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0.002,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0.001,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0.001,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0.001,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.312,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.313,"ShareLinkExtendedTest::testFindBySlugHit":0.311,"ShareLinkExtendedTest::testFindBySlugMiss":0,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.325,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0.001,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.318,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.319,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.311,"ShareLinkExtendedTest::testCreateWithLockedYear":0.311,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.313,"ShareLinkExtendedTest::testUpdateLockedYear":0.315,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.347,"ShareLinkExtendedTest::testIncrementUsage":0.336,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.322,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.316,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0.001,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0.001,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0.001,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":0.006,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0.001,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0.001,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0.001,"AutofocusFieldForErrorTest::testEditAutofocusYear":0,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0.001,"ThesisCreateValidationTest::testMissingTitleThrowsException":0.003,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0.001,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0.001,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0.001,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0.001,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0.001,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0.001,"ThesisCreateValidationTest::testYearZeroRejected":0.001,"ThesisCreateValidationTest::testYearBefore2000Rejected":0.001,"ThesisCreateValidationTest::testFarFutureYearRejected":0.001,"ThesisCreateValidationTest::testCurrentYearAccepted":0.001,"ThesisCreateValidationTest::testMalformedUrlRejected":0.001,"ThesisCreateValidationTest::testValidUrlAccepted":0,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0.001,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0.001,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0.001,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0.001,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0.001,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0.001,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0.001,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0.001,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.006,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.001,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0.001,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0.001,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0.001,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0.001,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0.001,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0.003,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0.001,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0.002,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0.001,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0.001,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0.001,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0.001,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0.001,"ErrorHandlerTest::testFkApPrograms":0,"ErrorHandlerTest::testFkFinalityTypes":0,"ErrorHandlerTest::testFkThesisLanguages":0.001,"ErrorHandlerTest::testFkThesisFormats":0,"ErrorHandlerTest::testFkThesisTags":0.001,"ErrorHandlerTest::testFkThesisSupervisors":0,"ErrorHandlerTest::testFkAccessTypes":0,"ErrorHandlerTest::testFkLicenseTypes":0,"ErrorHandlerTest::testFkAuthors":0,"ErrorHandlerTest::testFkQuotedTableName":0,"ErrorHandlerTest::testFkQuotedLanguages":0,"ErrorHandlerTest::testFkQuotedFormatTypes":0,"ErrorHandlerTest::testFkReferencesTags":0,"ErrorHandlerTest::testFkReferencesOrientations":0.001,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0.001,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0.001,"ErrorHandlerTest::testUniqueConstraint":0.001,"ErrorHandlerTest::testNotNullConstraint":0,"ErrorHandlerTest::testGenericPdoError":0.001,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0.001,"ErrorHandlerTest::testValidationExceptionPassesThrough":0,"ErrorHandlerTest::testGenericExceptionPassesThrough":0,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0,"ErrorHandlerTest::testLogWithContext":0.001,"ErrorHandlerTest::testLogWithNullValues":0,"ErrorHandlerTest::testLogWithEmptyExtra":0,"ErrorHandlerTest::testFkQuotedColumnNames":0.001,"ErrorHandlerTest::testFkUpdateStatement":0.001,"ErrorHandlerTest::testFkWithReferencesAndInsert":0,"PureLogicTest::testSplitJuryByRoleAllRoles":0.001,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0,"PureLogicTest::testSplitJuryByRoleEmptyJury":0,"PureLogicTest::testCollectCaptionPathsVttByMime":0,"PureLogicTest::testCollectCaptionPathsVttByExtension":0,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0,"PureLogicTest::testDetectFileTypeByMime":0,"PureLogicTest::testDetectFileTypeByExtensionFallback":0,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.006,"SearchControllerTest::testCoverMapContainsKnownThesis":0.003}} \ No newline at end of file +{"version":2,"defects":{"CryptoTest::testEncryptDecryptEmptyString":7,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":7,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":7,"SystemControllerHelpersTest::testHumanBytesOneMB":7,"SystemControllerHelpersTest::testHumanBytesOneGB":7,"CryptoTest::testEncryptEmptyStringProducesCiphertext":7,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":7,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":7,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":7,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":7,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":7,"RateLimitExtendedTest::testGetRemainingDecrements":7,"RateLimitExtendedTest::testGetRemainingAtLimit":7,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":7,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":7,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":8,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":8},"times":{"CryptoTest::testEncryptDecryptRoundTrip":0.029,"CryptoTest::testEncryptDecryptWithUnicode":0.002,"CryptoTest::testEncryptDecryptMultiline":0.002,"CryptoTest::testDifferentPlaintextsProduceDifferentCiphertexts":0.002,"CryptoTest::testSamePlaintextProducesDifferentCiphertexts":0.002,"CryptoTest::testIsEncryptedRecognizesEncryptedValue":0.002,"CryptoTest::testIsEncryptedRejectsPlaintext":0.003,"CryptoTest::testIsEncryptedReturnsFalseForEmptyString":0.004,"CryptoTest::testIsEncryptedRejectsInvalidBase64":0.002,"CryptoTest::testEncryptDecryptEmptyString":0.008,"CryptoTest::testDecryptEmptyStringReturnsEmpty":0.002,"CryptoTest::testDecryptInvalidBase64ReturnsInputGracefully":0.002,"CryptoTest::testDecryptTooShortBlobReturnsInputGracefully":0.002,"CryptoTest::testDecryptWithTamperedCiphertextReturnsEmpty":0.002,"CryptoTest::testDecryptValidBlobTamperedTagReturnsEmpty":0.002,"EmailObfuscatorTest::testEncodeContainsNoAtSign":0.019,"EmailObfuscatorTest::testEncodeOutputIsNumericEntities":0.005,"EmailObfuscatorTest::testEmailReturnsObfuscatedAddress":0.002,"EmailObfuscatorTest::testMailtoBuildsCorrectHrefStructure":0.002,"EmailObfuscatorTest::testEmailTextReplacesBareEmail":0.002,"EmailObfuscatorTest::testEmailTextReplacesMultipleEmails":0.002,"EmailObfuscatorTest::testMailtoInTextReplacesMailtoLinks":0.002,"EmailObfuscatorTest::testObfuscateHtmlReplacesAnchorTag":0.004,"EmailObfuscatorTest::testObfuscateHtmlKeepsNonMailtoLinksUnchanged":0.004,"EmailObfuscatorTest::testObfuscateHtmlPreservesCustomLinkText":0.002,"EmailObfuscatorTest::testEmptyStringReturnsEmpty":0.002,"EmailObfuscatorTest::testStringWithNoEmailsIsUnchanged":0.002,"EmailObfuscatorTest::testAlreadyObfuscatedContentIsNotDoubleEncoded":0.001,"EmailObfuscatorTest::testMultipleEmailsInOneString":0.002,"EmailObfuscatorTest::testEmailWithPlusSign":0.002,"StudentEmailTest::testBuildHtmlReturnsNonEmptyString":0.069,"StudentEmailTest::testBuildHtmlContainsKeyFields":0.003,"StudentEmailTest::testBuildHtmlEscapesSpecialCharacters":0.005,"StudentEmailTest::testBuildHtmlHandlesMissingOptionalFields":0.005,"StudentEmailTest::testBuildHtmlHandlesNullFieldsGracefully":0.007,"StudentEmailTest::testBuildHtmlHandlesEmptyArray":0.005,"StudentEmailTest::testBuildHtmlContainsLabelFields":0.004,"SystemControllerHelpersTest::testHumanBytesZero":0.175,"SystemControllerHelpersTest::testHumanBytesBelowOneKB":0.002,"SystemControllerHelpersTest::testHumanBytesOneKB":0.002,"SystemControllerHelpersTest::testHumanBytesOneMB":0.002,"SystemControllerHelpersTest::testHumanBytesOneGB":0.003,"SystemControllerHelpersTest::testHumanBytes1523MB":0.003,"SystemControllerHelpersTest::testHumanBytes2500GB":0.003,"SystemControllerHelpersTest::testDiskColorBelowWarning":0.003,"SystemControllerHelpersTest::testDiskColorWarning":0.003,"SystemControllerHelpersTest::testDiskColorCritical":0.002,"SystemControllerHelpersTest::testLogLineClassCrit":0.002,"SystemControllerHelpersTest::testLogLineClassError":0.002,"SystemControllerHelpersTest::testLogLineClassWarn":0.004,"SystemControllerHelpersTest::testLogLineClassNotice":0.004,"SystemControllerHelpersTest::testLogLineClassHttp500":0.003,"SystemControllerHelpersTest::testLogLineClassHttp300":0.004,"SystemControllerHelpersTest::testLogLineClassDefault":0.003,"SystemControllerHelpersTest::testNginxLineClassComment":0.003,"SystemControllerHelpersTest::testNginxLineClassBlock":0.004,"SystemControllerHelpersTest::testNginxLineClassDirective":0.003,"SystemControllerHelpersTest::testStatusLabelActive":0.002,"SystemControllerHelpersTest::testStatusLabelInactive":0.003,"SystemControllerHelpersTest::testStatusLabelFailed":0.004,"SystemControllerHelpersTest::testStatusLabelWarn":0.002,"SystemControllerHelpersTest::testStatusLabelUnknown":0.002,"SystemControllerHelpersTest::testStatusClassOk":0.002,"SystemControllerHelpersTest::testStatusClassWarn":0.004,"SystemControllerHelpersTest::testStatusClassError":0.002,"SystemControllerHelpersTest::testStatusClassUnknown":0.002,"TfeControllerOgTest::testBuildOgTagsReturnsAllRequiredKeys":0.002,"TfeControllerOgTest::testBuildOgTagsTitleIncludesAuthors":0.003,"TfeControllerOgTest::testBuildOgTagsImageEmptyWhenNoFiles":0.003,"TfeControllerOgTest::testBuildOgTagsImageFromCover":0.002,"TfeControllerOgTest::testBuildOgTagsImageFallbackToFirstImage":0.003,"TfeControllerOgTest::testBuildOgTagsUrlIncludesThesisId":0.003,"TfeControllerOgTest::testBuildOgTagsPublishedTimeFormatted":0.003,"TfeControllerOgTest::testBuildOgTagsPublishedTimeEmptyWhenNoYear":0.002,"TfeControllerOgTest::testBuildMetaDescriptionTruncatesLongSynopsis":0.005,"TfeControllerOgTest::testBuildMetaDescriptionKeepsShortSynopsis":0.004,"TfeControllerOgTest::testBuildMetaDescriptionEmptySynopsisReturnsDefault":0.003,"TfeControllerOgTest::testBuildMetaDescriptionStripsHtmlTags":0.003,"CryptoTest::testEncryptEmptyStringProducesCiphertext":0.003,"DatabaseExtendedTest::testEscapeLikeStringViaSearchConditions":0.003,"DatabaseExtendedTest::testBuildSearchConditionsEmptyParams":0.004,"DatabaseExtendedTest::testBuildSearchConditionsWithQuery":0.005,"DatabaseExtendedTest::testBuildSearchConditionsWithYear":0.003,"DatabaseExtendedTest::testBuildSearchConditionsWithAllFilters":0.002,"DatabaseExtendedTest::testFindDuplicateThesisExactMatch":0.003,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentTitle":0.003,"DatabaseExtendedTest::testFindDuplicateThesisMissesDifferentYear":0.004,"DatabaseExtendedTest::testFindDuplicateThesisEmptyAuthorNamesReturnsNull":0.002,"DatabaseExtendedTest::testFindDuplicateThesisEmptyTable":0.002,"DatabaseExtendedTest::testFindDuplicateThesisNearDuplicateByLevenshtein":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierFirstInYear":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierIncrementsCorrectly":0.002,"DatabaseExtendedTest::testGenerateThesisIdentifierUsesMaxNotCount":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsPaths":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesReturnsEmptyForUnknownIds":0.002,"DatabaseExtendedTest::testGetCoverPathsForThesesEmptyInputReturnsEmpty":0.004,"DatabaseExtendedTest::testGetCoverPathsForThesesMultipleTheses":0.003,"DatabaseExtendedTest::testFindOrCreateAuthorCreatesNew":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorIdempotent":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorWithEmail":0.002,"DatabaseExtendedTest::testFindOrCreateAuthorRejectsCSVArtefacts":0.002,"DatabaseExtendedTest::testDeduplicateLanguagesMergesCaseInsensitiveDupes":0.003,"DatabaseExtendedTest::testRenameLanguageUpdatesName":0.049,"DatabaseExtendedTest::testMergeLanguageReassignsTheses":0.003,"DatabaseExtendedTest::testRenameTagUpdatesName":0.003,"DatabaseExtendedTest::testMergeTagReassignsTheses":0.003,"RateLimitExtendedTest::testCheckKeyCountsPerKey":0.036,"RateLimitExtendedTest::testCheckKeyDoesNotAffectDefaultCheck":0.003,"RateLimitExtendedTest::testGetRemainingDecrements":0,"RateLimitExtendedTest::testGetRemainingAtLimit":0,"RateLimitExtendedTest::testGetRemainingUsesClientIdentifier":0.001,"RateLimitExtendedTest::testCheckUsesConsistentIdentifier":0,"RateLimitExtendedTest::testGetRemainingReturnsZeroAfterExhaustion":0.002,"RateLimitExtendedTest::testGetResetTimeReturnsZeroWhenNoData":0.002,"RateLimitExtendedTest::testGetResetTimePositiveAfterHits":0.003,"RateLimitExtendedTest::testCleanupRemovesOldFiles":0.002,"ShareLinkExtendedTest::testListActiveReturnsOnlyActiveLinks":0.351,"ShareLinkExtendedTest::testListArchivedReturnsOnlyArchivedLinks":0.29,"ShareLinkExtendedTest::testFindBySlugHit":0.292,"ShareLinkExtendedTest::testFindBySlugMiss":0.002,"ShareLinkExtendedTest::testSetPasswordAndDecryptRoundTrip":0.291,"ShareLinkExtendedTest::testGetDecryptedPasswordOnNonexistentId":0.002,"ShareLinkExtendedTest::testUpdateChangesNameAndExpiration":0.291,"ShareLinkExtendedTest::testUpdateOnlyNameLeavesExpirationUnchanged":0.29,"ShareLinkExtendedTest::testUpdateClearsExpiration":0.292,"ShareLinkExtendedTest::testCreateWithLockedYear":0.294,"ShareLinkExtendedTest::testCreateWithInvalidLockedYearRejected":0.292,"ShareLinkExtendedTest::testUpdateLockedYear":0.293,"ShareLinkExtendedTest::testUpdateClearLockedYear":0.294,"ShareLinkExtendedTest::testIncrementUsage":0.296,"ShareLinkExtendedTest::testCreateDefaultsToTfeWhenInvalidObjet":0.298,"ShareLinkExtendedTest::testCreateAcceptsValidObjet":0.329,"RateLimitExtendedTest::testGetRemainingStartsAtMax":0.002,"RateLimitExtendedTest::testCheckDecrementsRemainingForSameIp":0.002,"RateLimitExtendedTest::testCheckAndCheckKeyAreIndependent":0.002,"RateLimitExtendedTest::testMultipleChecksFromSameClient":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusTitle":1.23,"AutofocusFieldForErrorTest::testCreateAutofocusAuthors":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusSynopsis":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusYear":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusOrientation":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusAP":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusFinality":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLanguages":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusPromoteur":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurInterne":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusLecteurExterne":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusFormats":0.003,"AutofocusFieldForErrorTest::testCreateAutofocusLicense":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUrl":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusTags":0.002,"AutofocusFieldForErrorTest::testCreateAutofocusUnknownErrorReturnsNull":0.004,"AutofocusFieldForErrorTest::testEditAutofocusTitle":0.157,"AutofocusFieldForErrorTest::testEditAutofocusYear":0.002,"AutofocusFieldForErrorTest::testEditAutofocusSynopsis":0.002,"AutofocusFieldForErrorTest::testEditAutofocusAuthors":0.002,"AutofocusFieldForErrorTest::testEditAutofocusUnknownErrorReturnsNull":0.002,"AutofocusFieldForErrorTest::testCreateDoesNotLeakEditFieldNames":0.002,"ThesisCreateValidationTest::testValidSubmissionReturnsCleanedData":0.004,"ThesisCreateValidationTest::testMissingTitleThrowsException":0.005,"ThesisCreateValidationTest::testMissingAuthorsThrowsException":0.002,"ThesisCreateValidationTest::testMissingSynopsisThrowsException":0.004,"ThesisCreateValidationTest::testMissingOrientationInNonAdminModeThrowsException":0.003,"ThesisCreateValidationTest::testMissingAPProgramInNonAdminModeThrowsException":0.004,"ThesisCreateValidationTest::testMissingFinalityInNonAdminModeThrowsException":0.004,"ThesisCreateValidationTest::testInvalidYearFormatRejected":0.003,"ThesisCreateValidationTest::testYearZeroRejected":0.003,"ThesisCreateValidationTest::testYearBefore2000Rejected":0.003,"ThesisCreateValidationTest::testFarFutureYearRejected":0.003,"ThesisCreateValidationTest::testCurrentYearAccepted":0.004,"ThesisCreateValidationTest::testMalformedUrlRejected":0.004,"ThesisCreateValidationTest::testValidUrlAccepted":0.004,"ThesisCreateValidationTest::testDuplicateTagsAreDeduplicated":0.004,"ThesisCreateValidationTest::testMaxTenKeywordsEnforced":0.006,"ThesisCreateValidationTest::testXssPayloadStrippedFromTitle":0.004,"ThesisCreateValidationTest::testHtmlInSynopsisStripped":0.006,"ThesisCreateValidationTest::testMultipleAuthorsAreSorted":0.006,"ThesisCreateValidationTest::testMissingPromoteurInNonAdminModeThrowsException":0.006,"ThesisCreateValidationTest::testMissingLecteurInterneInNonAdminModeThrowsException":0.009,"ThesisCreateValidationTest::testMissingLanguagesInNonAdminModeThrowsException":0.011,"ThesisCreateValidationTest::testMissingFormatsInNonAdminModeThrowsException":0.006,"ThesisCreateValidationTest::testMissingLicenseWithLibreAccessThrowsException":0.008,"ThesisEditValidationTest::testLoadReturnsDataForKnownId":0.008,"ThesisEditValidationTest::testLoadThrowsOnUnknownId":0.01,"ThesisEditValidationTest::testLoadThrowsOnInvalidId":0.011,"ThesisEditValidationTest::testLoadThrowsOnNegativeId":0.014,"ThesisEditValidationTest::testCollectJuryMembersEmptyInput":0.007,"ThesisEditValidationTest::testCollectJuryMembersSinglePromoteur":0.006,"ThesisEditValidationTest::testCollectJuryMembersPromoteurUlb":0.004,"ThesisEditValidationTest::testCollectJuryMembersLecteurs":0.002,"ThesisEditValidationTest::testCollectJuryMembersDeduplicatesEmptyStrings":0.002,"ThesisEditValidationTest::testCollectJuryMembersScalarPromoteurAccepted":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlStoresValidUrl":0.007,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsInvalidUrl":0.003,"ThesisEditValidationTest::testHandleWebsiteUrlSkipsEmptyUrl":0.005,"ThesisEditValidationTest::testHandleWebsiteUrlNormalisesHttp":0.004,"ErrorHandlerTest::testFkThesesTableMentionsAllPossibleFields":0.032,"ErrorHandlerTest::testFkApPrograms":0.002,"ErrorHandlerTest::testFkFinalityTypes":0.002,"ErrorHandlerTest::testFkThesisLanguages":0.002,"ErrorHandlerTest::testFkThesisFormats":0.002,"ErrorHandlerTest::testFkThesisTags":0.003,"ErrorHandlerTest::testFkThesisSupervisors":0.002,"ErrorHandlerTest::testFkAccessTypes":0.002,"ErrorHandlerTest::testFkLicenseTypes":0.002,"ErrorHandlerTest::testFkAuthors":0.002,"ErrorHandlerTest::testFkQuotedTableName":0.002,"ErrorHandlerTest::testFkQuotedLanguages":0.003,"ErrorHandlerTest::testFkQuotedFormatTypes":0.002,"ErrorHandlerTest::testFkReferencesTags":0.002,"ErrorHandlerTest::testFkReferencesOrientations":0.002,"ErrorHandlerTest::testFkUnknownTableGenericFallback":0.002,"ErrorHandlerTest::testFkEmptyMessageGenericFallback":0.002,"ErrorHandlerTest::testUniqueConstraint":0.002,"ErrorHandlerTest::testNotNullConstraint":0.002,"ErrorHandlerTest::testGenericPdoError":0.002,"ErrorHandlerTest::testDuplicateThesisExceptionPassesThrough":0.006,"ErrorHandlerTest::testValidationExceptionPassesThrough":0.002,"ErrorHandlerTest::testGenericExceptionPassesThrough":0.002,"ErrorHandlerTest::testTypeErrorReturnsGeneric":0.001,"ErrorHandlerTest::testLogWithContext":0.003,"ErrorHandlerTest::testLogWithNullValues":0.002,"ErrorHandlerTest::testLogWithEmptyExtra":0.003,"ErrorHandlerTest::testFkQuotedColumnNames":0.003,"ErrorHandlerTest::testFkUpdateStatement":0.002,"ErrorHandlerTest::testFkWithReferencesAndInsert":0.002,"PureLogicTest::testSplitJuryByRoleAllRoles":0.046,"PureLogicTest::testSplitJuryByRoleEmptyNameSkipped":0.002,"PureLogicTest::testSplitJuryByRoleEmptyJury":0.002,"PureLogicTest::testCollectCaptionPathsVttByMime":0.003,"PureLogicTest::testCollectCaptionPathsVttByExtension":0.002,"PureLogicTest::testCollectCaptionPathsNoVttReturnsEmpty":0.002,"PureLogicTest::testDetectFileTypeByMime":0.002,"PureLogicTest::testDetectFileTypeByExtensionFallback":0.003,"SearchControllerTest::testHandleSearchReturnsCoverMapKey":0.059,"SearchControllerTest::testCoverMapContainsKnownThesis":0.005}} \ No newline at end of file diff --git a/TODO.md b/TODO.md index 9ea8100..b4613c1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,19 +1,33 @@ # TODO -> Last updated: 2026-06-15 -> Context: Multiple fixes for upload flow: CSRF staleness, adminOld return type, PHP upload limits, FormData crash, soft-deleted languages +> Last updated: 2026-06-19 +> Context: Analyse OverType editors on /admin/contenus-edit.php: concurrency safety, save reliability, content truncation bugs + +## In Progress +- [ ] #overtype-analysis Analyse and fix OverType editor reliability on contenus-edit.php ## Pending -- [ ] #apropos-toc-confirm Visually confirm charte + licence TOC layout renders correctly in browser +- [x] #tfe-optional-formats Make TFE file optional when format is Site web (1), Performance (4) or Installation (6) — fixed incorrect format IDs [3→1,4,6] + added client-side JS toggle for TFE required/asterisk. Note d'intention remains required. 🎯 `(fichiers-fragment.php, file-upload-filepond.js)` ✓ +- [x] #typography-weight-300 Set search placeholder + apropos/charte/licence

content to BBBDMSans weight 300 `(search.css, apropos.css)` ✓ +- [x] #toc-parts-uppercase Hardcode "PARTIES" uppercase + black bottom border on TOC label `(about.php, charte.php, licence.php, apropos.css)` ✓ +- [x] #apropos-overflow Prevent #apropos-intro and content-section children from overflowing `(apropos.css)` ✓ +- [x] #toc-navigation Fix TOC links not navigating to headings — enable `heading_permalink` extension in CommonMark with `id_prefix: ''`, `insert: 'before'`, `aria_hidden: true` + register extension on environment; use CommonMark's SlugNormalizer in extractToc; hide permalink anchors with CSS; add `min-width: 0` to `.content` to prevent grid overflow `(CharteController.php, LicenceController.php, MarkdownHelper.php, apropos.css)` ✓ + +- [x] #apropos-toc-style Fix TOC "Parties" label: Ductus font + lowercase, remove border-left from links, match global link style; rename .apropos-content → section.content, .apropos-section → .content-section, remove .prose wrapper `(apropos.css, about.php, charte.php, licence.php)` ✓ + +- [ ] #apropos-toc-confirm Visually confirm charte + licence TOC layout renders correctly in browser (dup after #apropos-toc-style) - [ ] #aria-test-manual Test WCAG changes with VoiceOver and NVDA on full add/edit/partage form flows - [ ] #nojs-upload-test Test end-to-end: submit partage form with JS disabled, verify files arrive via `$_FILES` - [ ] #csp-media-iframe-deploy Deploy nginx config fix to server, test PDF iframe on /tfe?id=221 ## Completed -- [x] #filepond-csrf-stale Fix FilePond CSRF token going stale when autosave rotates it `(file-upload-filepond.js)` ✓ +- [x] #csrf-rotation-race Stop CSRF token rotation in draft.php + remove hx-post from

— both broke FilePond uploads and form submission `(admin/actions/draft.php, partage/fragments/draft.php, FormBootstrap.php, pill-search.js)` ✓ +- [x] ~~#filepond-csrf-stale~~ (superseded by #csrf-rotation-race) - [x] #adminold-return-type Fix adminOld closure return type from `:string` to `:string|array` `(FormBootstrap.php)` ✓ +- [x] #duration-integer-units Make duration field: integer for pages/Mo, dedicated h/m/s time inputs `(form.php, ThesisCreateController.php, tfe.php, form-base.css)` ✓ +- [x] #licence-svg-fix Fix licence details/summary SVG: width 1rem, inline-flex layout `(fieldset-licence-explanation.php, form-base.css)` ✓ - [x] #restore-languages Un-soft-delete anglais (id=2) and néerlandais (id=71) in dev DB ✓ - [x] #php-upload-limits Increase PHP upload_max_filesize to 8G, post_max_size to 8.5G `(.user.ini)` ✓ - [x] #formdata-fieldset-crash Remove leftover debug console.log that called new FormData(fieldset) `(admin/footer.php)` ✓ diff --git a/app/public/admin/actions/draft.php b/app/public/admin/actions/draft.php index b84ea96..c46abdf 100644 --- a/app/public/admin/actions/draft.php +++ b/app/public/admin/actions/draft.php @@ -81,13 +81,17 @@ foreach ($_POST as $key => $value) { $_SESSION[$draftKey] = $draft; -// Rotate CSRF after mutation -$newToken = bin2hex(random_bytes(32)); -$_SESSION['csrf_token'] = $newToken; +// NOTE: Do NOT rotate the CSRF token here. +// Rotating it breaks concurrent requests: +// 1. FilePond uploads in flight use the old token (from ) +// and fail when the server session already has the new token. +// 2. Overlapping autosave requests hit CSRF mismatch. +// 3. HTMX fragment requests (pill-search, language-autre) can't use the old token. +// The CSRF token already rotates on page load and form submit — that's sufficient. +// Autosave is a background persistence mechanism and does not need token rotation. header('Content-Type: application/json'); echo json_encode([ - 'success' => true, - 'csrf_token' => $newToken, + 'success' => true, ]); exit; diff --git a/app/public/assets/css/apropos.css b/app/public/assets/css/apropos.css index 785efea..c695414 100644 --- a/app/public/assets/css/apropos.css +++ b/app/public/assets/css/apropos.css @@ -1,28 +1,22 @@ /* ============================================================ - À PROPOS PAGE (apropos.php) - Root class: .apropos-main + À PROPOS / CHARTE / LICENCE pages + Root class: .page-content ============================================================ */ -/* ------------------------------------------------------------------ */ -/* Page shell */ -/* ------------------------------------------------------------------ */ - -.apropos-main { - flex: 1; - min-height: 0; - overflow-y: auto; - padding: var(--space-xl) var(--space-l) var(--space-2xl); +/* Override body overflow:hidden — these pages use the viewport scrollbar + so that anchor navigation (#fragment) works natively. */ +.apropos-body { + overflow: auto; } -/* ------------------------------------------------------------------ */ -/* Two-column layout: sticky TOC nav | content */ -/* ------------------------------------------------------------------ */ - -.apropos-layout { +.page-content { + flex: 1; + min-height: 0; + scroll-behavior: smooth; + padding: var(--space-xl) var(--space-l) var(--space-2xl); display: grid; grid-template-columns: 180px 1fr; gap: var(--space-2xl); - width: 100%; align-items: start; } @@ -36,13 +30,13 @@ } .apropos-toc-label { - font-family: var(--font-body); + font-family: var(--font-display); font-size: var(--step--2); - font-weight: 600; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--text-tertiary); + font-weight: 400; + color: var(--text-primary); margin: 0 0 var(--space-2xs) 0; + padding-bottom: var(--space-2xs); + border-bottom: 1px solid var(--text-primary); } .apropos-toc ul { @@ -62,13 +56,10 @@ display: block; padding: var(--space-3xs) 0; transition: color 0.15s; - border-left: 2px solid transparent; - padding-left: var(--space-2xs); } .apropos-toc ul a:hover { color: var(--accent-primary); - border-left-color: var(--accent-primary); } .apropos-toc-link:first-of-type { @@ -96,19 +87,115 @@ /* Right — main content area */ /* ------------------------------------------------------------------ */ -.apropos-content { - display: flex; - flex-direction: column; - gap: 0; +.page-content > .content, +.page-content > .content-section { + grid-column: 2; + min-width: 0; + max-width: 100%; } -.apropos-section { +/* Shared typography for about-page sections and charte/licence content */ +.content, +.content-section { + display: block; + max-width: 100%; + font-family: var(--font-body); + font-size: var(--step-0); + line-height: 1.6; + color: var(--text-primary); + font-weight: 300; +} + +.content *, +.content-section * { + max-width: 100%; + overflow-wrap: anywhere; + word-break: break-word; +} + +.content p, +.content-section p { + margin: 0 0 1em 0; +} + +.content p:last-child, +.content-section p:last-child { + margin-bottom: 0; +} + +.content :where(h1, h2, h3), +.content-section :where(h1, h2, h3) { + margin: 1.5em 0 0.5em 0; +} + +.content a, +.content-section a { + color: inherit; + text-decoration: none; + font-weight: 700; +} + +.content a:hover, +.content-section a:hover { + color: var(--accent-primary); + text-decoration: none; +} + +.content ul, +.content ol, +.content-section ul, +.content-section ol { + padding-left: var(--space-m); + margin-bottom: var(--space-s); +} + +.content li, +.content-section li { + margin-bottom: 0.3em; +} + +.content strong, +.content-section strong { + font-weight: 700; +} + +.content em, +.content-section em { + font-style: italic; +} + +.content :where(pre, pre code, code), +.content-section :where(pre, pre code, code) { + display: block; + max-width: 100%; + overflow-x: auto; + overflow-wrap: normal; + word-break: normal; + font-family: "Courier New", Courier, monospace; + font-size: 0.88em; + background: var(--bg-tertiary); + padding: 0.5em 0.75em; + border-radius: var(--radius); + white-space: pre-wrap; + word-wrap: break-word; +} + +#apropos-intro *, +#apropos-contacts *, +#apropos-credits * { + overflow-wrap: anywhere; + word-break: break-word; + max-width: 100%; +} + +/* Section separators (about page only — .content-section adds dividers) */ +.page-content > .content-section { padding-bottom: var(--space-xl); border-bottom: 1px solid var(--border-primary); margin-bottom: var(--space-xl); } -.apropos-section:last-child { +.page-content > .content-section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; @@ -118,7 +205,7 @@ /* Section titles */ /* ------------------------------------------------------------------ */ -.apropos-section-title { +.content-section-title { font-family: var(--font-display); font-size: var(--step-3); font-weight: 400; @@ -127,59 +214,9 @@ line-height: 1.1; } -/* ------------------------------------------------------------------ */ -/* Intro prose — Markdown-rendered content */ -/* ------------------------------------------------------------------ */ - -.prose { - font-family: var(--font-body); - font-size: var(--step-0); - line-height: 1.6; - color: var(--text-primary); - font-weight: 400; -} - -.prose p { - margin: 0 0 1em 0; -} - -.prose p:last-child { - margin-bottom: 0; -} - -.prose :where(h1, h2, h3) { - margin: 1.5em 0 0.5em 0; -} - -.prose a { - color: var(--accent-primary); - text-decoration: underline; - text-underline-offset: 2px; -} - -.prose ul, -.prose ol { - padding-left: var(--space-m); - margin-bottom: var(--space-s); -} - -.prose li { - margin-bottom: 0.3em; -} - -.prose strong { - font-weight: 700; -} -.prose em { - font-style: italic; -} - -.prose code { - font-family: "Courier New", Courier, monospace; - font-size: 0.88em; - background: var(--bg-tertiary); - padding: 0.1em 0.3em; - border-radius: var(--radius); +/* Hide CommonMark heading permalink anchors (needed only for their id attr) */ +.heading-permalink { + display: none; } /* ------------------------------------------------------------------ */ @@ -220,15 +257,15 @@ .apropos-contact-card a { font-size: var(--step--1); - color: var(--accent-primary); - text-decoration: underline; - text-underline-offset: 2px; - transition: opacity 0.15s; + color: inherit; + text-decoration: none; + font-weight: 700; + transition: color 0.15s; } .apropos-contact-card a:hover { color: var(--accent-primary); - opacity: 1; + text-decoration: none; } /* ------------------------------------------------------------------ */ @@ -284,7 +321,7 @@ /* ------------------------------------------------------------------ */ @media (max-width: 900px) { - .apropos-layout { + .page-content { grid-template-columns: 1fr; gap: var(--space-l); } @@ -309,32 +346,27 @@ gap: var(--space-xs); } - .apropos-toc ul a { - border-left: none; - padding-left: 0; - } - .apropos-toc-link { border-top: none; padding-top: 0; margin-left: auto; } - .prose { + .content-section { font-size: var(--step-0); } - .apropos-section-title { + .content-section-title { font-size: var(--step-2); } } @media (max-width: 600px) { - .apropos-main { + .page-content { padding: var(--space-m) var(--space-s) var(--space-xl); } - .prose { + .content-section { font-size: var(--step-0); } diff --git a/app/public/assets/css/colors.css b/app/public/assets/css/colors.css index 868f319..0c12034 100644 --- a/app/public/assets/css/colors.css +++ b/app/public/assets/css/colors.css @@ -38,10 +38,10 @@ --accent-red: #f25a5a; /* Gradient (header) */ - --gradient-1: #3c856c; - --gradient-2: #60ecb4; - --gradient-3: #e390ff; - --gradient-4: #9557b5; + --gradient-1: #42963f; + --gradient-2: #65e478; + --gradient-3: #57abc7; + --gradient-4: #db53ed; /* Header decorative */ --header-gradient-fade: rgba(149, 87, 181, 0); diff --git a/app/public/assets/css/components/header.css b/app/public/assets/css/components/header.css index 4ba40f5..b9801ec 100644 --- a/app/public/assets/css/components/header.css +++ b/app/public/assets/css/components/header.css @@ -8,12 +8,13 @@ header { vertical-align: center; flex-shrink: 0; + background: #9557B5; background: linear-gradient( - 180deg, - var(--gradient-1) 0%, - var(--gradient-2) 33%, - var(--gradient-3) 66%, - var(--gradient-4) 100% + 0deg, + rgba(149, 87, 181, 1) 0%, + rgba(192, 93, 225, 1) 25%, + rgba(51, 191, 135, 1) 75%, + rgba(60, 133, 108, 1) 100% ); } @@ -60,7 +61,6 @@ header nav ul a[aria-current="page"] { padding-bottom: 1px; } - /* ── Logo ───────────────────────────────────────────────────────────── */ .nav-logo { @@ -130,8 +130,12 @@ header nav ul a[aria-current="page"] { transition: all 0.3s ease-out; } -.navicon::before { top: -7px; } -.navicon::after { bottom: -7px; } +.navicon::before { + top: -7px; +} +.navicon::after { + bottom: -7px; +} /* ── Mobile (≤ 640px) ───────────────────────────────────────────────── */ @@ -160,11 +164,14 @@ header nav ul a[aria-current="page"] { } header nav[aria-label="Navigation principale"] - .nav-left-links li:not(:first-child) { + .nav-left-links + li:not(:first-child) { display: none; } - .menu-icon { display: flex; } + .menu-icon { + display: flex; + } header nav[aria-label="Navigation principale"] .nav-mobile-links { display: block; diff --git a/app/public/assets/css/components/search.css b/app/public/assets/css/components/search.css index 1babafb..21ef117 100644 --- a/app/public/assets/css/components/search.css +++ b/app/public/assets/css/components/search.css @@ -6,7 +6,7 @@ .header-search-wrap { padding: 0; flex-shrink: 0; - background: linear-gradient(180deg, var(--gradient-4) 0%, #ffffffee 100%); + background: linear-gradient(180deg, #9557B5 0%, #ffffffee 100%); } .header-search-form { width: 100%; } @@ -40,4 +40,6 @@ .header-search-input-wrap input::placeholder { color: var(--accent-primary) !important; + font-family: var(--font-body); + font-weight: 300; } diff --git a/app/public/assets/css/form-base.css b/app/public/assets/css/form-base.css index 9919682..4517206 100644 --- a/app/public/assets/css/form-base.css +++ b/app/public/assets/css/form-base.css @@ -352,6 +352,34 @@ border-radius: var(--radius); } +.duration-time-inputs { + display: flex; + align-items: flex-end; + gap: var(--space-xs); +} + +.duration-time-fields { + display: inline-flex; + align-items: center; + gap: var(--space-3xs); +} + +.duration-time-fields span { + font-size: var(--step--1); + color: var(--text-secondary); +} + +.licence-details { + display: inline-flex; +} + +.licence-summary { + display: inline-flex; + align-items: center; + gap: var(--space-3xs); + cursor: pointer; +} + .licence-degree h4 { margin: 0 0 var(--space-2xs); font-weight: 600; diff --git a/app/public/assets/css/public.css b/app/public/assets/css/public.css index a6d4ebe..1178235 100644 --- a/app/public/assets/css/public.css +++ b/app/public/assets/css/public.css @@ -111,8 +111,8 @@ background: linear-gradient( 180deg, rgba(60, 133, 108, 1) 0%, - rgba(96, 236, 180, 1) 33%, - rgba(227, 144, 255, 1) 66%, + rgba(51, 191, 135, 1) 25%, + rgba(192, 93, 225, 1) 75%, rgba(149, 87, 181, 1) 100% ); } diff --git a/app/public/assets/css/repertoire.css b/app/public/assets/css/repertoire.css index 5fedb51..ed7b9d8 100644 --- a/app/public/assets/css/repertoire.css +++ b/app/public/assets/css/repertoire.css @@ -223,6 +223,56 @@ gap: var(--space-3xs); } +.result-card__cover { + margin: 0; +} + +.result-card__cover img { + width: 100%; + aspect-ratio: 4/3; + object-fit: cover; + display: block; + border-radius: 7px 7px 0 0; +} + +.result-card__gradient { + width: 100%; + aspect-ratio: 4/3; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-s); + text-align: center; + box-sizing: border-box; + border-radius: 7px 7px 0 0; + background: linear-gradient( + 180deg, + rgba(60, 133, 108, 1) 0%, + rgba(51, 191, 135, 1) 25%, + rgba(192, 93, 225, 1) 75%, + rgba(149, 87, 181, 1) 100% + ); +} + +.result-card__gradient-author { + color: var(--accent-foreground); + font-size: var(--step--2); + opacity: 0.85; + margin-bottom: 0.25rem; + display: block; +} + +.result-card__gradient-title { + color: var(--accent-foreground); + font-size: var(--step--1); + font-weight: 600; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + .result-card__authors { font-size: var(--step--1); font-weight: 500; @@ -391,8 +441,8 @@ background: linear-gradient( 180deg, rgba(60, 133, 108, 1) 0%, - rgba(96, 236, 180, 1) 33%, - rgba(227, 144, 255, 1) 66%, + rgba(51, 191, 135, 1) 25%, + rgba(192, 93, 225, 1) 75%, rgba(149, 87, 181, 1) 100% ); display: flex; diff --git a/app/public/assets/js/app/file-upload-filepond.js b/app/public/assets/js/app/file-upload-filepond.js index e92aa6b..e0e773a 100644 --- a/app/public/assets/js/app/file-upload-filepond.js +++ b/app/public/assets/js/app/file-upload-filepond.js @@ -643,6 +643,7 @@ enableFilepondMode(); _xamxamFilepondReady = false; window.XamxamInitFilePonds(); + if (window.XamxamUpdateTfeRequired) window.XamxamUpdateTfeRequired(); setTimeout(() => { _xamxamFilepondReady = true; }, 0); @@ -694,6 +695,68 @@ } }); + // ── TFE file optional when Site web (1), Performance (4) or Installation (6) ── + // The format checkboxes no longer trigger HTMX swaps; this JS toggles the TFE + // required attribute and asterisk client-side so the student sees immediate feedback. + // admin_mode hidden input (value="1") suppresses required toggling for admins. + (function () { + var optionalFormatIds = ["1", "4", "6"]; + + function isAdminMode() { + var el = document.querySelector('input[name="admin_mode"]'); + return el && el.value === "1"; + } + + function updateTfeRequired() { + if (isAdminMode()) return; + + var tfeInput = document.getElementById("tfe-files-input"); + if (!tfeInput) return; + + var checkedAny = false; + var boxes = document.querySelectorAll('input[name="formats[]"]:checked'); + for (var i = 0; i < boxes.length; i++) { + if (optionalFormatIds.indexOf(boxes[i].value) !== -1) { + checkedAny = true; + break; + } + } + + // Find the label for the TFE input (its parent group's -
- Info +
+ Info

- Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l’erg. Je suis conscient des responsabilités et obligations légales qui viennent avec une diffusion externe – et acquiesce avoir lu la documentation prévue à cet effet par l'erg, ainsi qu'avoir discuté des enjeux d'une publication avec l'équipe pédagogique. J'accepte de partager mes droits de diffusion avec l'erg, ce uniquement dans le cadre d'une diffusion sur la plateforme xamxam. + Mon TFE est en libre accès à tout le monde sur la plateforme des TFE ainsi que dans la bibliothèque de l'erg. Je suis conscient des responsabilités et obligations légales qui viennent avec une diffusion externe – et acquiesce avoir lu la documentation prévue à cet effet par l'erg, ainsi qu'avoir discuté des enjeux d'une publication avec l'équipe pédagogique. J'accepte de partager mes droits de diffusion avec l'erg, ce uniquement dans le cadre d'une diffusion sur la plateforme xamxam.

@@ -85,10 +85,10 @@ $adminMode = $adminMode ?? false; 🔒 Interne
-
- Info +
+ Info

- Mon TFE et ma note d'intention ne sont accessibles que sur place en physique ainsi que sur la plateforme xamxam par la communauté erg. Une note descriptive est disponible sur le site à toustes. J’autorise une (ré-)utilisation et diffusion dans un contexte académique et didactique au sein de l'erg. + Mon TFE et ma note d'intention ne sont accessibles que sur place en physique ainsi que sur la plateforme xamxam par la communauté erg. Une note descriptive est disponible sur le site à toustes. J'autorise une (ré-)utilisation et diffusion dans un contexte académique et didactique au sein de l'erg.

@@ -108,10 +108,10 @@ $adminMode = $adminMode ?? false;
-
- Info +
+ Info

- Mon TFE n’est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site. + Mon TFE n'est pas disponible en physique ni sur le site. Une note descriptive est disponible sur le site.

@@ -121,14 +121,14 @@ $adminMode = $adminMode ?? false; + once htmx renders them - later DOM order wins in POST. --> - +
-
+ +
Durée -
+
-
- - + + +
- Optionnel. Exemples : 88 pages, 32 minutes, 1.5 heures, 120 Mo. -
+ + + Optionnel. Exemples : 88 pages, 120 Mo, 1h30.
+
+ + diff --git a/app/templates/public/about.php b/app/templates/public/about.php index 1bd6f1f..6dd0ce8 100644 --- a/app/templates/public/about.php +++ b/app/templates/public/about.php @@ -31,97 +31,87 @@ function renderEntries(array $entries): string $suffix = implode(" & ", array_slice($parts, -2)); return $prefix !== "" ? $prefix . ", " . $suffix : $suffix; } ?> -
-
- - - - - -
- - -
-
- -
-
+
+ + + + +
+ +
+ + +
+

Contacts

+
+ +
+ + + + + !empty($e), + ); + foreach ($emails as $email): ?> + + +
+ +
+
+ + + +
+

Crédits

+
+
+
Design & développement
+
+ Olivia Marly, + Théophile Gerveau-Mercier & + Théo Hennequin +
+
+ +
+
Iconographie
+
+ Phosphor Icons — + MIT, + par Helena Zhang et Tobias Fried +
+
+
+
-
diff --git a/app/templates/public/charte.php b/app/templates/public/charte.php index 10ecf6a..80e1975 100644 --- a/app/templates/public/charte.php +++ b/app/templates/public/charte.php @@ -1,30 +1,23 @@ -
-
+
- - - + + + + + +
+ + + +

Contenu à venir.

- - -
-
-
- - - -

Contenu à venir.

- -
-
-
-
+
diff --git a/app/templates/public/licence.php b/app/templates/public/licence.php index 10ecf6a..80e1975 100644 --- a/app/templates/public/licence.php +++ b/app/templates/public/licence.php @@ -1,30 +1,23 @@ -
-
+
- - - + + + + + +
+ + + +

Contenu à venir.

- - -
-
-
- - - -

Contenu à venir.

- -
-
-
-
+
diff --git a/app/templates/public/search.php b/app/templates/public/search.php index f5bd01c..ec06029 100644 --- a/app/templates/public/search.php +++ b/app/templates/public/search.php @@ -76,15 +76,22 @@
    -
  • +
  • Couverture — <?= htmlspecialchars($item['title']) ?>
    + +
    + + +
    + + ·
  • diff --git a/app/templates/public/tfe.php b/app/templates/public/tfe.php index c300fc9..5153e44 100644 --- a/app/templates/public/tfe.php +++ b/app/templates/public/tfe.php @@ -50,22 +50,25 @@ 'pages', - 'minutes' => 'minutes', - 'sec' => 'secondes', - 'heures' => 'heures', - 'mo' => 'Mo', - ]; - $_label = $_unitLabels[$_dUnit] ?? $_dUnit; - // if float, show 0.1 or .0 as needed - $_display = ($_dVal == (int)$_dVal) ? (int)$_dVal : $_dVal; + $_label = match($_dUnit) { + 'pages' => 'pages', + 'mo' => 'Mo', + 'durée' => '', + default => $_dUnit, + }; + if ($_dUnit === 'durée') { + $_hours = (int)floor($_dVal); + $_mins = (int)round(($_dVal - $_hours) * 60); + $_display = ($_mins > 0) ? "{$_hours}h{$_mins}" : "{$_hours}h"; + } else { + $_display = ($_dVal == (int)$_dVal) ? (int)$_dVal : $_dVal; + } ?>

    Durée : - +

    - +