first commit
This commit is contained in:
166
assets/js/media-admin.js
Normal file
166
assets/js/media-admin.js
Normal file
@@ -0,0 +1,166 @@
|
||||
(function (window, document) {
|
||||
'use strict';
|
||||
|
||||
var page = document.getElementById('media-admin-page');
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
|
||||
var uploadForm = document.getElementById('media-upload-form');
|
||||
var uploadButton = document.getElementById('media-upload-submit');
|
||||
var uploadInput = document.getElementById('media-upload-input');
|
||||
var feedback = document.getElementById('media-upload-feedback');
|
||||
var isPickerMode = page.dataset.pickerMode === '1';
|
||||
|
||||
function setFeedback(message, isError) {
|
||||
if (!feedback) {
|
||||
return;
|
||||
}
|
||||
|
||||
feedback.textContent = message;
|
||||
feedback.style.color = isError ? '#b91c1c' : '';
|
||||
}
|
||||
|
||||
async function copyToClipboard(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return;
|
||||
}
|
||||
|
||||
var helper = document.createElement('textarea');
|
||||
helper.value = text;
|
||||
helper.setAttribute('readonly', 'readonly');
|
||||
helper.style.position = 'absolute';
|
||||
helper.style.left = '-9999px';
|
||||
document.body.appendChild(helper);
|
||||
helper.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(helper);
|
||||
}
|
||||
|
||||
function toAbsoluteUrl(url) {
|
||||
try {
|
||||
return new URL(String(url || ''), window.location.origin).href;
|
||||
} catch (error) {
|
||||
return String(url || '');
|
||||
}
|
||||
}
|
||||
|
||||
function buildImageHtml(url, mediaId) {
|
||||
return '<img src="' + String(url)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>') + '" alt="" data-media-id="' + Number(mediaId) + '">';
|
||||
}
|
||||
|
||||
function flashButtonLabel(button, message, isError) {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!button.dataset.originalLabel) {
|
||||
button.dataset.originalLabel = button.textContent.trim();
|
||||
}
|
||||
|
||||
if (button.dataset.restoreTimerId) {
|
||||
window.clearTimeout(Number(button.dataset.restoreTimerId));
|
||||
}
|
||||
|
||||
button.textContent = message;
|
||||
|
||||
if (isError) {
|
||||
button.classList.add('btn--danger');
|
||||
}
|
||||
|
||||
button.dataset.restoreTimerId = String(window.setTimeout(function () {
|
||||
button.textContent = button.dataset.originalLabel || '';
|
||||
button.classList.remove('btn--danger');
|
||||
delete button.dataset.restoreTimerId;
|
||||
}, 1800));
|
||||
}
|
||||
|
||||
async function handleUpload() {
|
||||
if (!uploadForm || !uploadButton || !uploadInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!uploadInput.files || uploadInput.files.length === 0) {
|
||||
setFeedback('Sélectionnez une image avant de téléverser.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
uploadButton.disabled = true;
|
||||
setFeedback('Téléversement en cours…', false);
|
||||
|
||||
try {
|
||||
var response = await fetch(uploadForm.dataset.uploadUrl || uploadForm.action || window.location.href, {
|
||||
method: 'POST',
|
||||
body: new FormData(uploadForm),
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
var payload = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || 'Le téléversement a échoué.');
|
||||
}
|
||||
|
||||
setFeedback('Image téléversée. Rafraîchissement…', false);
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setFeedback(error instanceof Error ? error.message : 'Le téléversement a échoué.', true);
|
||||
} finally {
|
||||
uploadButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadButton) {
|
||||
uploadButton.addEventListener('click', function () {
|
||||
handleUpload();
|
||||
});
|
||||
}
|
||||
|
||||
page.addEventListener('click', async function (event) {
|
||||
var button = event.target.closest('[data-media-action]');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
var action = button.dataset.mediaAction;
|
||||
var url = button.dataset.mediaUrl || '';
|
||||
var mediaId = Number(button.dataset.mediaId || '0');
|
||||
|
||||
if (action === 'insert-editor') {
|
||||
if (isPickerMode && window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'netslim:media-selected',
|
||||
url: url,
|
||||
mediaId: mediaId,
|
||||
html: buildImageHtml(url, mediaId)
|
||||
}, window.location.origin);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'copy-url') {
|
||||
await copyToClipboard(toAbsoluteUrl(url));
|
||||
flashButtonLabel(button, 'URL copiée.', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'copy-html') {
|
||||
await copyToClipboard(buildImageHtml(url, mediaId));
|
||||
flashButtonLabel(button, 'HTML copié.', false);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
flashButtonLabel(button, 'Copie impossible.', true);
|
||||
setFeedback('Impossible de copier dans le presse-papiers.', true);
|
||||
}
|
||||
});
|
||||
})(window, document);
|
||||
147
assets/js/post-editor-media-picker.js
Normal file
147
assets/js/post-editor-media-picker.js
Normal file
@@ -0,0 +1,147 @@
|
||||
(function (window, document, $) {
|
||||
'use strict';
|
||||
|
||||
if (!$ || typeof $.fn.trumbowyg !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
var editorElement = document.getElementById('editor');
|
||||
var modal = document.getElementById('media-picker-modal');
|
||||
var closeButton = document.getElementById('media-picker-close');
|
||||
var frame = document.getElementById('media-picker-frame');
|
||||
|
||||
if (!editorElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $editor = $(editorElement);
|
||||
var previousActiveElement = null;
|
||||
|
||||
function escapeHtmlAttribute(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function createMediaHtml(url, mediaId) {
|
||||
return '<img src="' + escapeHtmlAttribute(url) + '" alt="" data-media-id="' + Number(mediaId) + '">';
|
||||
}
|
||||
|
||||
function ensurePickerLoaded() {
|
||||
if (!frame || !frame.dataset.pickerSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frame.getAttribute('src')) {
|
||||
frame.setAttribute('src', frame.dataset.pickerSrc);
|
||||
}
|
||||
}
|
||||
|
||||
function focusToolbarButton() {
|
||||
var button = document.querySelector('.trumbowyg-mediaPicker-button');
|
||||
if (button) {
|
||||
button.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousActiveElement = document.activeElement;
|
||||
$editor.trumbowyg('saveRange');
|
||||
ensurePickerLoaded();
|
||||
modal.hidden = false;
|
||||
modal.classList.add('is-open');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
|
||||
if (closeButton) {
|
||||
closeButton.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function closePicker() {
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
modal.classList.remove('is-open');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modal.hidden = true;
|
||||
|
||||
if (previousActiveElement instanceof HTMLElement) {
|
||||
previousActiveElement.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
focusToolbarButton();
|
||||
}
|
||||
|
||||
function insertSelectedMedia(url, mediaId) {
|
||||
if (!url || !mediaId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$editor.trumbowyg('restoreRange');
|
||||
$editor.trumbowyg('execCmd', {
|
||||
cmd: 'insertHTML',
|
||||
param: createMediaHtml(url, mediaId)
|
||||
});
|
||||
closePicker();
|
||||
$editor.trumbowyg('saveRange');
|
||||
}
|
||||
|
||||
$editor.trumbowyg({
|
||||
lang: 'fr',
|
||||
removeformatPasted: true,
|
||||
btnsDef: {
|
||||
mediaPicker: {
|
||||
fn: function () {
|
||||
openPicker();
|
||||
return true;
|
||||
},
|
||||
ico: 'upload',
|
||||
title: 'Médiathèque'
|
||||
}
|
||||
},
|
||||
btns: [
|
||||
['viewHTML'],
|
||||
['formatting'],
|
||||
['strong', 'em', 'underline', 'del'],
|
||||
['link', 'mediaPicker'],
|
||||
['justifyLeft', 'justifyCenter', 'justifyRight'],
|
||||
['unorderedList', 'orderedList'],
|
||||
['insertHorizontalRule'],
|
||||
['fullscreen']
|
||||
]
|
||||
});
|
||||
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', closePicker);
|
||||
}
|
||||
|
||||
if (modal) {
|
||||
modal.addEventListener('click', function (event) {
|
||||
if (event.target === modal) {
|
||||
closePicker();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape' && modal && !modal.hidden) {
|
||||
closePicker();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
if (event.origin !== window.location.origin || !event.data || event.data.type !== 'netslim:media-selected') {
|
||||
return;
|
||||
}
|
||||
|
||||
insertSelectedMedia(event.data.url, event.data.mediaId);
|
||||
});
|
||||
})(window, document, window.jQuery);
|
||||
16
assets/scss/base/_reset.scss
Normal file
16
assets/scss/base/_reset.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Reset / base globale
|
||||
// =============================================================
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: $font-family-base;
|
||||
font-size: $font-size-base;
|
||||
color: $color-text;
|
||||
margin: $spacing-xl;
|
||||
}
|
||||
113
assets/scss/base/_typography.scss
Normal file
113
assets/scss/base/_typography.scss
Normal file
@@ -0,0 +1,113 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Typographie — styles globaux du texte
|
||||
// =============================================================
|
||||
// Échelle typographique de référence pour les éléments HTML sémantiques.
|
||||
// Les composants surchargent ces valeurs si leur contexte l'exige
|
||||
// (ex: .card__title, .error-page__code définissent leur propre taille).
|
||||
// Les styles de contenu éditeur (Trumbowyg) sont dans components/_rich-text.scss.
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Titres
|
||||
// -------------------------------------------------------------
|
||||
// h1 : titre d'article (detail.twig) et logo du site (site-header__logo)
|
||||
// h2 : titres de pages et de sections (toutes les vues admin et auth)
|
||||
// h3 : sous-titres de blocs (admin-create__title)
|
||||
// h4-h6 : non utilisés dans les templates, définis en filet de sécurité
|
||||
// pour le contenu HTML inséré via Trumbowyg
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-3xl;
|
||||
font-weight: $font-weight-bold;
|
||||
line-height: $line-height-tight;
|
||||
margin: 0 0 $spacing-md;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: $font-weight-bold;
|
||||
line-height: $line-height-tight;
|
||||
margin: 0 0 $spacing-md;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-bold;
|
||||
line-height: $line-height-snug;
|
||||
margin: 0 0 $spacing-sm;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-bold;
|
||||
line-height: $line-height-base;
|
||||
margin: 0 0 $spacing-sm;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Liens
|
||||
// -------------------------------------------------------------
|
||||
// Liens nus sans classe BEM : navigation intra-page, liens de retour,
|
||||
// "Mot de passe oublié ?", liens du footer.
|
||||
// Les liens dans les composants (.card__title-link, .btn…) surchargent.
|
||||
|
||||
a {
|
||||
color: $color-primary;
|
||||
text-decoration: underline;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Éléments inline
|
||||
// -------------------------------------------------------------
|
||||
// small : hints de formulaire ("Minimum 12 caractères"), métadonnées d'articles
|
||||
small {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
// code : slugs dans l'admin (categories/index.twig), URLs dans les médias
|
||||
code {
|
||||
font-family: monospace, monospace;
|
||||
font-size: $font-size-sm;
|
||||
background: $color-bg-light;
|
||||
padding: $spacing-2xs 0.3em;
|
||||
border-radius: $radius-sm;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// pre : blocs de code dans le contenu Trumbowyg
|
||||
pre {
|
||||
font-family: monospace, monospace;
|
||||
font-size: $font-size-sm;
|
||||
background: $color-bg-light;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border-radius: $radius-md;
|
||||
overflow-x: auto;
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Séparateurs
|
||||
// -------------------------------------------------------------
|
||||
// hr : séparateur dans detail.twig (après l'article) et form.twig (avant les métadonnées)
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid $color-border;
|
||||
margin: $spacing-lg 0;
|
||||
}
|
||||
57
assets/scss/components/_admin-create.scss
Normal file
57
assets/scss/components/_admin-create.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Boîte d'ajout (admin)
|
||||
// =============================================================
|
||||
// Bloc réutilisable pour les encarts "Ajouter …" des pages admin.
|
||||
// Inspiré du style historique de l'écran /admin/categories.
|
||||
|
||||
.admin-create {
|
||||
margin-bottom: $spacing-xl;
|
||||
padding: $spacing-md;
|
||||
background: $color-bg-light;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
|
||||
.admin-create__title {
|
||||
margin: 0 0 $spacing-md;
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
.admin-create__form {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: $spacing-sm;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-create__label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-xs;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.admin-create__input {
|
||||
min-width: 260px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-create__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.admin-create__hint {
|
||||
margin: $spacing-sm 0 0;
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.admin-create__feedback {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
24
assets/scss/components/_alert.scss
Normal file
24
assets/scss/components/_alert.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Alertes / messages flash
|
||||
// =============================================================
|
||||
|
||||
.alert {
|
||||
padding: $spacing-md;
|
||||
border-radius: $radius-md;
|
||||
margin-bottom: $spacing-md;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&--danger {
|
||||
background: $color-danger-bg;
|
||||
border-color: $color-danger-border;
|
||||
color: $color-danger-text;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: $color-success-bg;
|
||||
border-color: $color-success-border;
|
||||
color: $color-success-text;
|
||||
}
|
||||
}
|
||||
52
assets/scss/components/_badge.scss
Normal file
52
assets/scss/components/_badge.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Badges — bloc composant .badge pour rôles utilisateur et catégories
|
||||
// =============================================================
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-xs;
|
||||
padding: $spacing-2xs $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-semibold;
|
||||
line-height: $line-height-tight;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.badge--admin {
|
||||
color: $color-warning-text;
|
||||
background: $color-warning-bg;
|
||||
}
|
||||
|
||||
.badge--editor {
|
||||
color: $color-info-text;
|
||||
background: $color-info-bg;
|
||||
}
|
||||
|
||||
.badge--user {
|
||||
color: $color-text-muted;
|
||||
background: $color-bg-light;
|
||||
}
|
||||
|
||||
// Badge catégorie — affiché sur les cartes d'articles et les pages de détail
|
||||
// Cliquable : redirige vers la liste filtrée par catégorie
|
||||
.badge--category {
|
||||
color: $color-primary;
|
||||
background: $color-primary-bg;
|
||||
vertical-align: middle;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
background: $color-primary-bg-hover;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
}
|
||||
}
|
||||
100
assets/scss/components/_button.scss
Normal file
100
assets/scss/components/_button.scss
Normal file
@@ -0,0 +1,100 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
@use "sass:color";
|
||||
|
||||
// =============================================================
|
||||
// Boutons — bloc composant .btn
|
||||
// =============================================================
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-xs;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $radius-md;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
line-height: $line-height-tight;
|
||||
font: inherit;
|
||||
font-weight: $font-weight-semibold;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&--disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background: $color-primary;
|
||||
color: $color-bg-white;
|
||||
|
||||
&:hover {
|
||||
background: color.scale($color-primary, $lightness: -14%);
|
||||
}
|
||||
}
|
||||
|
||||
.btn--secondary {
|
||||
background: $color-text-muted;
|
||||
color: $color-bg-white;
|
||||
|
||||
&:hover {
|
||||
background: color.scale($color-text-muted, $lightness: -14%);
|
||||
}
|
||||
}
|
||||
|
||||
.btn--danger {
|
||||
background: $color-danger;
|
||||
color: $color-bg-white;
|
||||
|
||||
&:hover {
|
||||
background: color.scale($color-danger, $lightness: -12%);
|
||||
}
|
||||
}
|
||||
|
||||
.btn--sm {
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
// Modificateur taille — boutons principaux dans les formulaires centrés
|
||||
.btn--lg {
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
}
|
||||
|
||||
// Modificateur largeur — occupe toute la largeur de son conteneur
|
||||
.btn--full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Variante de lien textuel.
|
||||
// Utiliser la combinaison BEM `.btn.btn--link`.
|
||||
.btn--link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $color-primary;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
line-height: $line-height-base;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $focus-ring-color;
|
||||
outline-offset: 2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
197
assets/scss/components/_card.scss
Normal file
197
assets/scss/components/_card.scss
Normal file
@@ -0,0 +1,197 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Composant carte — générique
|
||||
// =============================================================
|
||||
// Définit deux blocs BEM :
|
||||
// .card-list — conteneur de liste de cartes
|
||||
// .card — carte individuelle (vignette + contenu)
|
||||
// Aucune référence au domaine métier : les contenus spécifiques
|
||||
// (extrait d'article, prix produit…) sont surchargés dans les
|
||||
// fichiers de module (modules/post/_listing.scss, etc.).
|
||||
|
||||
// Conteneur de liste de cartes
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
// Modificateur : fond grisé encadrant les cartes (ex: page d'accueil)
|
||||
// Rend les box-shadow perceptibles en créant un contraste avec le fond blanc des cartes
|
||||
.card-list--contained {
|
||||
background: $color-bg-light;
|
||||
padding: $spacing-md;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
|
||||
// Carte : vignette à gauche, corps à droite
|
||||
// La vignette est toujours présente (image ou initiales)
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-lg;
|
||||
padding: $spacing-lg;
|
||||
background: $color-bg-white;
|
||||
border-radius: $radius-md;
|
||||
box-shadow: 0 1px 4px $color-card-shadow;
|
||||
}
|
||||
|
||||
// Lien englobant la vignette — pas de décoration, tabindex=-1 pour
|
||||
// éviter la double tabulation (le titre est déjà un lien cliquable)
|
||||
.card__thumb-link {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Vignette image
|
||||
.card__thumb {
|
||||
width: 180px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: $radius-md;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Vignette initiales (affiché quand l'entité n'a pas d'image)
|
||||
// Mêmes dimensions que .card__thumb pour un alignement cohérent
|
||||
.card__initials {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 180px;
|
||||
height: 120px;
|
||||
border-radius: $radius-md;
|
||||
background: $color-bg-initials;
|
||||
color: $color-text-muted;
|
||||
font-size: $font-size-display;
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: 0.05em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Wrapper externe : prend la place à droite de la vignette
|
||||
// Organisé en flex colonne pour empiler .card__body et .card__actions
|
||||
.card__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// Partie textuelle de la carte
|
||||
// max-height contraint le titre + métadonnées + aperçu à la hauteur de la vignette.
|
||||
// overflow:hidden clip l'aperçu si le contenu dépasse — .card__actions est en dehors
|
||||
// de ce conteneur et reste donc toujours visible.
|
||||
.card__body {
|
||||
max-height: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Titre de la carte
|
||||
.card__title {
|
||||
margin: 0 0 $spacing-xs;
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
|
||||
// Lien du titre — élément BEM dédié plutôt qu'un sélecteur descendant sur <a>
|
||||
.card__title-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// Métadonnées (date, auteur, prix…)
|
||||
.card__meta {
|
||||
margin-bottom: $spacing-sm;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
// Texte court de présentation — clipé par overflow:hidden du parent .card__body.
|
||||
// word-break empêche le débordement de mots longs ou d'URLs sans espace.
|
||||
.card__excerpt {
|
||||
margin: $spacing-sm 0;
|
||||
color: $color-text;
|
||||
line-height: $line-height-base;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
// Styles pour le HTML formaté retourné par post_excerpt()
|
||||
// Les listes à puces/numérotées sont compactées pour rester dans l'aperçu
|
||||
ul,
|
||||
ol {
|
||||
margin: $spacing-xs 0 0 $spacing-sm;
|
||||
padding-left: $spacing-sm;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0;
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
em,
|
||||
i {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
// Zone d'actions (liens, boutons) — aspect défini dans le fichier de page
|
||||
.card__actions {
|
||||
margin-top: $spacing-sm;
|
||||
}
|
||||
|
||||
// Adaptation mobile : vignette au-dessus du corps, pleine largeur
|
||||
// max-height est annulé sur .card__body — en disposition colonne l'aperçu
|
||||
// peut s'étendre librement sous la vignette.
|
||||
@include mobile {
|
||||
.card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card__thumb-link {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card__thumb,
|
||||
.card__initials {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
// En mobile (disposition colonne), le corps s'étend librement
|
||||
.card__body {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
// Titre légèrement réduit pour éviter qu'il soit trop imposant sur petit écran
|
||||
.card__title {
|
||||
font-size: $font-size-md;
|
||||
}
|
||||
}
|
||||
26
assets/scss/components/_empty-state.scss
Normal file
26
assets/scss/components/_empty-state.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
.empty-state {
|
||||
padding: $spacing-lg;
|
||||
border: 1px dashed $color-border;
|
||||
border-radius: $radius-md;
|
||||
background: $color-bg-light;
|
||||
|
||||
&__title {
|
||||
margin: 0 0 $spacing-xs;
|
||||
line-height: $line-height-snug;
|
||||
}
|
||||
|
||||
&__message {
|
||||
margin: 0;
|
||||
color: $color-text-muted;
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
margin-top: $spacing-md;
|
||||
}
|
||||
}
|
||||
119
assets/scss/components/_form-container.scss
Normal file
119
assets/scss/components/_form-container.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Formulaires
|
||||
// =============================================================
|
||||
// Convention retenue :
|
||||
// - .form-container est un bloc métier-agnostique réutilisable
|
||||
// - les utilitaires de layout vivent dans utilities/_inline.scss
|
||||
|
||||
.form-container {
|
||||
max-width: $layout-content-max-width;
|
||||
margin: 0 auto;
|
||||
|
||||
&__panel {
|
||||
background: $color-bg-white;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-lg;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__intro {
|
||||
margin: $spacing-sm 0 0;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
&__field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__field--editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-xs;
|
||||
font-weight: $font-weight-regular;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
margin: $spacing-xs 0 0;
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
&__input,
|
||||
&__select,
|
||||
&__textarea {
|
||||
width: 100%;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
background: $color-bg-white;
|
||||
font: inherit;
|
||||
font-weight: $font-weight-regular;
|
||||
line-height: $line-height-base;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
}
|
||||
}
|
||||
|
||||
&__select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__textarea {
|
||||
min-height: 14rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
align-items: center;
|
||||
margin-top: $spacing-sm;
|
||||
}
|
||||
|
||||
&__action {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: $spacing-lg;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
&__input--disabled {
|
||||
background: $color-bg-light;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
|
||||
.form-container--narrow {
|
||||
max-width: 420px;
|
||||
margin: $spacing-xl auto;
|
||||
}
|
||||
59
assets/scss/components/_media-picker-modal.scss
Normal file
59
assets/scss/components/_media-picker-modal.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Media picker modal — fenêtre de sélection des médias (Trumbowyg)
|
||||
// =============================================================
|
||||
|
||||
.media-picker-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-lg;
|
||||
// Overlay plus sombre que la charte "card" pour bien isoler le focus.
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.media-picker-modal.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.media-picker-modal__dialog {
|
||||
width: min(1100px, 100%);
|
||||
height: min(820px, calc(100vh - #{$spacing-2xl}));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: $color-bg-white;
|
||||
border-radius: $radius-lg;
|
||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.28);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.media-picker-modal__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-md ($spacing-md + $spacing-xs);
|
||||
border-bottom: 1px solid $color-border;
|
||||
}
|
||||
|
||||
.media-picker-modal__title {
|
||||
margin: 0;
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
.media-picker-modal__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: $color-bg-light;
|
||||
}
|
||||
|
||||
.media-picker-modal__frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: $color-bg-white;
|
||||
}
|
||||
86
assets/scss/components/_pagination.scss
Normal file
86
assets/scss/components/_pagination.scss
Normal file
@@ -0,0 +1,86 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Pagination
|
||||
// =============================================================
|
||||
// Bloc partagé car rendu via un partial Twig utilisé sur plusieurs écrans.
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-sm;
|
||||
margin-top: $spacing-lg;
|
||||
}
|
||||
|
||||
.pagination__summary {
|
||||
color: $color-text-muted;
|
||||
font-size: $font-size-sm;
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
|
||||
.pagination__pages {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.pagination__link,
|
||||
.pagination__control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.25rem;
|
||||
text-decoration: none;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination__link {
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
color: $color-text;
|
||||
background: $color-bg-white;
|
||||
|
||||
&:hover {
|
||||
border-color: $color-primary;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&--current {
|
||||
border-color: $color-primary;
|
||||
color: $color-primary;
|
||||
background: $color-primary-bg;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination__control--disabled {
|
||||
color: $color-text-subtle;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.pagination {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pagination__summary,
|
||||
.pagination__control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination__pages {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
82
assets/scss/components/_rich-text.scss
Normal file
82
assets/scss/components/_rich-text.scss
Normal file
@@ -0,0 +1,82 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Contenu riche partagé
|
||||
// =============================================================
|
||||
// Exception assumée au BEM : le HTML interne est généré par Trumbowyg et
|
||||
// n'est pas entièrement piloté par les templates Twig. Les sélecteurs
|
||||
// descendants sont donc réservés à .rich-text et à .trumbowyg-editor.
|
||||
|
||||
.rich-text,
|
||||
.trumbowyg-editor {
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
p, ul, ol, blockquote, pre {
|
||||
margin: 0 0 $spacing-md;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: $spacing-md;
|
||||
border-left: 3px solid $color-border;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: $radius-md;
|
||||
margin: $spacing-sm 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid $color-border;
|
||||
padding: $spacing-sm;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.trumbowyg-box,
|
||||
.trumbowyg-editor,
|
||||
.trumbowyg-button-pane {
|
||||
font: inherit;
|
||||
font-weight: $font-weight-regular;
|
||||
}
|
||||
|
||||
.trumbowyg-box,
|
||||
.trumbowyg-editor {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.trumbowyg-box {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: $radius-md;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-within {
|
||||
@include focus-ring;
|
||||
}
|
||||
}
|
||||
|
||||
.trumbowyg-button-pane button {
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $focus-ring-color;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
89
assets/scss/components/_search-bar.scss
Normal file
89
assets/scss/components/_search-bar.scss
Normal file
@@ -0,0 +1,89 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
@use "sass:color";
|
||||
|
||||
// =============================================================
|
||||
// Barre de recherche — bloc .search-bar
|
||||
// =============================================================
|
||||
// Bloc autonome réutilisable pour les listes filtrables.
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.search-bar__input {
|
||||
flex: 1;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
font: inherit;
|
||||
line-height: $line-height-base;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar__btn {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
background: $color-primary;
|
||||
color: $color-bg-white;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $radius-md;
|
||||
font: inherit;
|
||||
font-weight: $font-weight-semibold;
|
||||
line-height: $line-height-tight;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
background: color.scale($color-primary, $lightness: -16%);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar__reset {
|
||||
color: $color-text-muted;
|
||||
text-decoration: none;
|
||||
font-size: $font-size-sm;
|
||||
padding: $spacing-xs;
|
||||
line-height: $line-height-none;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
color: $color-danger;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar__info {
|
||||
margin: (-$spacing-sm) 0 $spacing-md;
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.search-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-bar__btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
79
assets/scss/components/_upload.scss
Normal file
79
assets/scss/components/_upload.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Bloc upload — aperçu et actions sur les médias uploadés
|
||||
// =============================================================
|
||||
|
||||
.upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.upload__thumb-link {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.upload__thumb {
|
||||
display: block;
|
||||
width: 96px;
|
||||
height: 72px;
|
||||
object-fit: cover;
|
||||
border-radius: $radius-sm;
|
||||
border: 1px solid $color-border;
|
||||
background: $color-bg-light;
|
||||
}
|
||||
|
||||
.upload__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-xs;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.upload__url {
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
color: $color-text-muted;
|
||||
margin-bottom: $spacing-xs;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.upload__thumb-link--compact {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.upload__thumb--compact {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
// Variante bouton — utile en mode picker si on veut rendre la vignette cliquable
|
||||
.upload__thumb-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
}
|
||||
32
assets/scss/core/_mixins.scss
Normal file
32
assets/scss/core/_mixins.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
@use "variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Mixins
|
||||
// =============================================================
|
||||
|
||||
// Breakpoint mobile — au-dessous de cette largeur, les composants
|
||||
// basculent en layout colonne (ex: .card)
|
||||
@mixin mobile {
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
// Transitions interactives cohérentes sur les composants cliquables
|
||||
@mixin interactive-transition {
|
||||
transition:
|
||||
color $transition-fast,
|
||||
background-color $transition-fast,
|
||||
border-color $transition-fast,
|
||||
box-shadow $transition-fast,
|
||||
opacity $transition-fast,
|
||||
transform $transition-fast,
|
||||
text-decoration-color $transition-fast;
|
||||
}
|
||||
|
||||
// Ring de focus accessible et homogène
|
||||
@mixin focus-ring {
|
||||
outline: none;
|
||||
border-color: $color-primary;
|
||||
box-shadow: 0 0 0 $focus-ring-width $focus-ring-color;
|
||||
}
|
||||
104
assets/scss/core/_variables.scss
Normal file
104
assets/scss/core/_variables.scss
Normal file
@@ -0,0 +1,104 @@
|
||||
// =============================================================
|
||||
// Variables — design tokens du projet
|
||||
// =============================================================
|
||||
// Ce fichier est la source de vérité pour toutes les valeurs
|
||||
// de couleur, typographie et espacement utilisées dans le projet.
|
||||
// Modifier une valeur ici la propage à l'ensemble des composants.
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Couleurs principales
|
||||
// -------------------------------------------------------------
|
||||
|
||||
$color-primary: #007bff;
|
||||
$color-primary-bg: #cce5ff;
|
||||
$color-primary-bg-hover: #b8daff;
|
||||
$color-danger: #dc3545;
|
||||
$color-success-bg: #d4edda;
|
||||
$color-success-border: #c3e6cb;
|
||||
$color-success-text: #155724;
|
||||
$color-danger-bg: #f8d7da;
|
||||
$color-danger-border: #f5c6cb;
|
||||
$color-danger-text: #721c24;
|
||||
$color-warning-bg: #fff3cd;
|
||||
$color-warning-text: #856404;
|
||||
$color-info-bg: #d1ecf1;
|
||||
$color-info-text: #0c5460;
|
||||
$color-card-shadow: rgba(0, 0, 0, 0.07);
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Couleurs neutres
|
||||
// -------------------------------------------------------------
|
||||
|
||||
$color-text: #212529;
|
||||
$color-text-muted: #6c757d;
|
||||
$color-text-subtle: #aaa;
|
||||
$color-bg-light: #f8f9fa;
|
||||
$color-bg-white: #ffffff;
|
||||
$color-bg-initials: #e9ecef;
|
||||
$color-border: #dee2e6;
|
||||
$color-border-light: #ccc;
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Typographie
|
||||
// -------------------------------------------------------------
|
||||
|
||||
$font-family-base: Arial, sans-serif;
|
||||
$font-size-xs: 0.85rem;
|
||||
$font-size-sm: 0.875rem;
|
||||
$font-size-base: 1rem;
|
||||
$font-size-md: 1.05rem;
|
||||
$font-size-lg: 1.15rem;
|
||||
$font-size-xl: 1.2rem;
|
||||
$font-size-2xl: 1.4rem;
|
||||
$font-size-3xl: 1.75rem;
|
||||
$font-size-display: 2rem;
|
||||
$font-size-display-lg: 4rem;
|
||||
$font-size-footer: 0.9rem;
|
||||
$line-height-none: 1;
|
||||
$line-height-tight: 1.2;
|
||||
$line-height-snug: 1.3;
|
||||
$line-height-base: 1.5;
|
||||
$font-weight-regular: 400;
|
||||
$font-weight-semibold: 600;
|
||||
$font-weight-bold: 700;
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Espacements
|
||||
// -------------------------------------------------------------
|
||||
|
||||
$spacing-2xs: 0.125rem;
|
||||
$spacing-xs: 0.25rem;
|
||||
$spacing-sm: 0.5rem;
|
||||
$spacing-md: 1rem;
|
||||
$spacing-lg: 1.5rem;
|
||||
$spacing-xl: 2rem;
|
||||
$spacing-2xl: 3rem;
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Bordures
|
||||
// -------------------------------------------------------------
|
||||
|
||||
$radius-sm: 3px;
|
||||
$radius-md: 4px;
|
||||
$radius-lg: 8px;
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// États interactifs
|
||||
// -------------------------------------------------------------
|
||||
|
||||
$focus-ring-width: 2px;
|
||||
$focus-ring-color: rgba(0, 123, 255, 0.16);
|
||||
$transition-fast: 0.15s ease;
|
||||
$transition-base: 0.2s ease;
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Layout
|
||||
// -------------------------------------------------------------
|
||||
|
||||
$layout-content-max-width: 960px;
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Responsive
|
||||
// -------------------------------------------------------------
|
||||
|
||||
$breakpoint-mobile: 600px;
|
||||
19
assets/scss/layout/_picker-layout.scss
Normal file
19
assets/scss/layout/_picker-layout.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Layout picker — pages embarquées (ex: sélecteur de médias)
|
||||
// =============================================================
|
||||
|
||||
body.picker-layout {
|
||||
margin: 0;
|
||||
background: $color-bg-light;
|
||||
}
|
||||
|
||||
.picker-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.picker-layout__inner {
|
||||
padding: $spacing-md;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
13
assets/scss/layout/_site-footer.scss
Normal file
13
assets/scss/layout/_site-footer.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Footer
|
||||
// =============================================================
|
||||
|
||||
.site-footer {
|
||||
margin-top: $spacing-xl;
|
||||
padding-top: $spacing-md;
|
||||
border-top: 1px solid $color-border-light;
|
||||
color: $color-text-muted;
|
||||
font-size: $font-size-footer;
|
||||
}
|
||||
88
assets/scss/layout/_site-header.scss
Normal file
88
assets/scss/layout/_site-header.scss
Normal file
@@ -0,0 +1,88 @@
|
||||
@use "../core/variables" as *;
|
||||
@use "../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Header
|
||||
// =============================================================
|
||||
|
||||
.site-header {
|
||||
border-bottom: 1px solid $color-border-light;
|
||||
padding: $spacing-md 0;
|
||||
margin-bottom: $spacing-xl;
|
||||
}
|
||||
|
||||
.site-header__inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site-header__logo {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Lien englobant le titre du blog — élément BEM dédié plutôt qu'un sélecteur descendant sur <a>
|
||||
.site-header__logo-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.site-header__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
// Nom d'utilisateur connecté dans le header
|
||||
.site-header__user {
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
// Élément d'action cliquable dans le header (lien ou bouton)
|
||||
.site-header__action {
|
||||
text-decoration: underline;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: $color-primary;
|
||||
font: inherit;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $focus-ring-color;
|
||||
outline-offset: 2px;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.site-header__inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.site-header__nav {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
}
|
||||
17
assets/scss/layout/_site-main.scss
Normal file
17
assets/scss/layout/_site-main.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Main
|
||||
// =============================================================
|
||||
|
||||
.site-main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.site-main__inner {
|
||||
width: 100%;
|
||||
max-width: $layout-content-max-width;
|
||||
margin: 0 auto;
|
||||
padding-inline: $spacing-md;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
43
assets/scss/main.scss
Normal file
43
assets/scss/main.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
// =============================================================
|
||||
// Point d'entrée — importe tous les partiels dans l'ordre
|
||||
// =============================================================
|
||||
// Règle d'organisation frontend :
|
||||
// - core : design tokens et mixins, jamais de CSS généré
|
||||
// - base : HTML global (reset, typographie native)
|
||||
// - components : blocs BEM réutilisables et exceptions contrôlées
|
||||
// - layout : structure globale de page (header, main, footer)
|
||||
// - modules : styles propres à un domaine ou à un écran métier
|
||||
// - utilities : helpers ponctuels préfixés .u-
|
||||
//
|
||||
// Ce fichier reste un simple orchestrateur d'imports.
|
||||
|
||||
@use "core/variables" as *;
|
||||
@use "core/mixins" as *;
|
||||
|
||||
@use "base/reset";
|
||||
@use "base/typography";
|
||||
|
||||
@use "components/button";
|
||||
@use "components/alert";
|
||||
@use "components/badge";
|
||||
@use "components/card";
|
||||
@use "components/empty-state";
|
||||
@use "components/admin-create";
|
||||
@use "components/form-container";
|
||||
@use "components/search-bar";
|
||||
@use "components/upload";
|
||||
@use "components/pagination";
|
||||
@use "components/rich-text";
|
||||
@use "components/media-picker-modal";
|
||||
|
||||
@use "layout/site-header";
|
||||
@use "layout/site-main";
|
||||
@use "layout/picker-layout";
|
||||
@use "layout/site-footer";
|
||||
|
||||
@use "modules/shared/admin" as admin-shared;
|
||||
@use "modules/shared/error-page";
|
||||
@use "modules/post/listing";
|
||||
@use "modules/post/post";
|
||||
|
||||
@use "utilities/inline";
|
||||
69
assets/scss/modules/post/_listing.scss
Normal file
69
assets/scss/modules/post/_listing.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
@use "../../core/variables" as *;
|
||||
@use "../../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Page d'accueil — liste des articles
|
||||
// =============================================================
|
||||
// Les styles de structure des cartes sont dans components/_card.scss.
|
||||
// Ce fichier surcharge uniquement les éléments spécifiques au contexte blog.
|
||||
|
||||
// Lien d'action "Lire la suite →" — élément BEM dédié plutôt qu'un sélecteur
|
||||
// descendant sur <a>, pour respecter BEM et éviter les collisions de styles
|
||||
.card__actions-link {
|
||||
font-size: $font-size-sm;
|
||||
color: $color-primary;
|
||||
text-decoration: none;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Barre de filtre par catégorie
|
||||
// =============================================================
|
||||
|
||||
// Conteneur de la liste de liens de filtre
|
||||
.category-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
margin-bottom: $spacing-lg;
|
||||
padding-bottom: $spacing-md;
|
||||
border-bottom: 1px solid $color-border;
|
||||
}
|
||||
|
||||
// Lien de filtre individuel
|
||||
.category-filter__item {
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
text-decoration: none;
|
||||
color: $color-text-muted;
|
||||
border: 1px solid $color-border;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
}
|
||||
}
|
||||
|
||||
// État actif — catégorie sélectionnée
|
||||
.category-filter__item--active {
|
||||
color: $color-primary;
|
||||
border-color: $color-primary;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
29
assets/scss/modules/post/_post.scss
Normal file
29
assets/scss/modules/post/_post.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
@use "../../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Bloc article — page de détail
|
||||
// =============================================================
|
||||
// Ce fichier ne porte que la coque du bloc .post.
|
||||
// Les styles de contenu riche partagé (HTML Trumbowyg rendu en front
|
||||
// et zone d'édition WYSIWYG) vivent dans components/_rich-text.scss.
|
||||
|
||||
.post {
|
||||
padding: $spacing-md 0;
|
||||
|
||||
&__title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin-bottom: $spacing-sm;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
&__updated {
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.post__back {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
161
assets/scss/modules/shared/_admin.scss
Normal file
161
assets/scss/modules/shared/_admin.scss
Normal file
@@ -0,0 +1,161 @@
|
||||
@use "../../core/variables" as *;
|
||||
@use "../../core/mixins" as *;
|
||||
|
||||
// =============================================================
|
||||
// Administration partagée
|
||||
// =============================================================
|
||||
// Blocs BEM utilisés sur plusieurs écrans d'administration.
|
||||
|
||||
.admin-nav {
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.admin-page-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-md;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
&__body {
|
||||
display: grid;
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__intro {
|
||||
margin: 0;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: $spacing-sm;
|
||||
border-bottom: 1px solid $color-border;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
|
||||
th {
|
||||
background: $color-bg-light;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
&__self {
|
||||
color: $color-text-muted;
|
||||
font-size: $font-size-xs;
|
||||
}
|
||||
|
||||
&__muted {
|
||||
color: $color-text-subtle;
|
||||
font-size: $font-size-xs;
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__list {
|
||||
margin: $spacing-xs 0 0;
|
||||
padding-left: $spacing-md;
|
||||
}
|
||||
|
||||
&__list-item + &__list-item {
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
&__code {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__role-select {
|
||||
font-size: $font-size-sm;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
background: $color-bg-white;
|
||||
cursor: pointer;
|
||||
|
||||
@include interactive-transition;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-ring;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.admin-table {
|
||||
display: block;
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody,
|
||||
tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
tr {
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius-md;
|
||||
margin-bottom: $spacing-md;
|
||||
padding: $spacing-sm;
|
||||
background: $color-bg-white;
|
||||
}
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-sm;
|
||||
padding: $spacing-xs 0;
|
||||
border-bottom: 1px solid $color-border;
|
||||
font-size: $font-size-sm;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: attr(data-label);
|
||||
font-weight: $font-weight-semibold;
|
||||
min-width: 100px;
|
||||
flex-shrink: 0;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-actions {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
21
assets/scss/modules/shared/_error-page.scss
Normal file
21
assets/scss/modules/shared/_error-page.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
@use "../../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Page d'erreur partagée
|
||||
// =============================================================
|
||||
|
||||
.error-page {
|
||||
text-align: center;
|
||||
padding: $spacing-xl 0;
|
||||
}
|
||||
|
||||
.error-page__code {
|
||||
font-size: $font-size-display-lg;
|
||||
margin-bottom: $spacing-sm;
|
||||
color: $color-text-muted;
|
||||
}
|
||||
|
||||
.error-page__message {
|
||||
font-size: $font-size-xl;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
19
assets/scss/utilities/_inline.scss
Normal file
19
assets/scss/utilities/_inline.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@use "../core/variables" as *;
|
||||
|
||||
// =============================================================
|
||||
// Utilitaires
|
||||
// =============================================================
|
||||
// Préfixe obligatoire : .u-
|
||||
// Utiliser seulement pour des ajustements de layout ponctuels, jamais
|
||||
// pour porter un style visuel métier ou remplacer un bloc BEM.
|
||||
|
||||
.u-inline-form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.u-inline-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
Reference in New Issue
Block a user