Frise horizontale + mode sombre
Cartes cote a cote (defilement horizontal infini) Zone En attente separee en bas Dark mode toggle avec persistence localStorage
This commit is contained in:
+234
-122
@@ -8,111 +8,184 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Lora', Georgia, serif; background: #faf6f0; color: #2c1810; min-height: 100vh; }
|
||||
|
||||
/* ── Light theme (default) ── */
|
||||
:root {
|
||||
--bg: #faf6f0;
|
||||
--bg-alt: #f3ece2;
|
||||
--bg-card: #fff;
|
||||
--bg-input: #faf6f0;
|
||||
--border: #e0d5c7;
|
||||
--border-light: #f0ebe3;
|
||||
--text: #2c1810;
|
||||
--text-muted: #8a7968;
|
||||
--text-hint: #a08a76;
|
||||
--text-light: #c4b5a4;
|
||||
--accent: #8a7060;
|
||||
--accent-hover: #5c3d2e;
|
||||
--brown: #5c3d2e;
|
||||
--terra: #b85c3a;
|
||||
--olive: #7a5c40;
|
||||
--blue-grey: #6a7a8a;
|
||||
--shadow: rgba(92,61,46,0.06);
|
||||
--shadow-hover: rgba(92,61,46,0.12);
|
||||
--overlay: rgba(44,24,16,0.4);
|
||||
}
|
||||
|
||||
/* ── Dark theme ── */
|
||||
.dark {
|
||||
--bg: #1a1614;
|
||||
--bg-alt: #231f1c;
|
||||
--bg-card: #2a2420;
|
||||
--bg-input: #1a1614;
|
||||
--border: #3d3530;
|
||||
--border-light: #332c27;
|
||||
--text: #e8ddd0;
|
||||
--text-muted: #a09080;
|
||||
--text-hint: #807060;
|
||||
--text-light: #5a4e44;
|
||||
--accent: #c4a882;
|
||||
--accent-hover: #e0c8a8;
|
||||
--brown: #c4a882;
|
||||
--terra: #d4845a;
|
||||
--olive: #a08a6a;
|
||||
--blue-grey: #8a9aaa;
|
||||
--shadow: rgba(0,0,0,0.2);
|
||||
--shadow-hover: rgba(0,0,0,0.35);
|
||||
--overlay: rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
body { font-family: 'Lora', Georgia, serif; background: var(--bg); color: var(--text); min-height: 100vh; transition: background 0.3s, color 0.3s; }
|
||||
|
||||
/* Layout */
|
||||
.app { display: flex; min-height: 100vh; }
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar { width: 240px; background: #f3ece2; border-right: 1px solid #e0d5c7; padding: 24px 0; flex-shrink: 0; display: flex; flex-direction: column; }
|
||||
.sidebar-logo { padding: 0 22px 20px; font-family: 'Playfair Display', serif; font-size: 19px; font-weight: 700; color: #5c3d2e; }
|
||||
.sidebar-project { padding: 10px 16px; margin: 0 14px 18px; background: #fff; border-radius: 10px; font-size: 14px; color: #5c3d2e; border: 1px solid #e0d5c7; }
|
||||
.nav-item { padding: 10px 18px; margin: 2px 14px; border-radius: 10px; font-size: 15px; color: #8a7968; cursor: pointer; display: flex; align-items: center; gap: 10px; }
|
||||
.nav-item:hover { background: #ebe3d7; color: #5c3d2e; }
|
||||
.nav-item.active { background: #e8ddd0; color: #5c3d2e; font-weight: 500; }
|
||||
.nav-icon { width: 32px; height: 32px; font-size: 16px; background: #e8ddd0; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.sidebar-footer { margin-top: auto; padding: 16px 22px; font-size: 11px; color: #a08a76; }
|
||||
.sidebar { width: 220px; background: var(--bg-alt); border-right: 1px solid var(--border); padding: 24px 0; flex-shrink: 0; display: flex; flex-direction: column; }
|
||||
.sidebar-logo { padding: 0 22px 20px; font-family: 'Playfair Display', serif; font-size: 18px; font-weight: 700; color: var(--brown); }
|
||||
.sidebar-project { padding: 10px 16px; margin: 0 14px 18px; background: var(--bg-card); border-radius: 10px; font-size: 14px; color: var(--brown); border: 1px solid var(--border); }
|
||||
.nav-item { padding: 10px 18px; margin: 2px 14px; border-radius: 10px; font-size: 15px; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; gap: 10px; }
|
||||
.nav-item:hover { background: var(--border); color: var(--text); }
|
||||
.nav-item.active { background: var(--border); color: var(--text); font-weight: 500; }
|
||||
.nav-icon { width: 30px; height: 30px; font-size: 15px; background: var(--border); border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.sidebar-footer { margin-top: auto; padding: 16px 22px; font-size: 11px; color: var(--text-hint); }
|
||||
|
||||
/* Main */
|
||||
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
/* Topbar */
|
||||
.topbar { height: 54px; background: #fff; border-bottom: 1px solid #e0d5c7; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; }
|
||||
.topbar { height: 52px; background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 24px; flex-shrink: 0; }
|
||||
.topbar-title { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; }
|
||||
.topbar-right { display: flex; align-items: center; gap: 12px; }
|
||||
.btn-new { padding: 7px 16px; background: #8a7060; color: #faf6f0; border: none; border-radius: 8px; font-size: 13px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; }
|
||||
.btn-new:hover { background: #5c3d2e; }
|
||||
.btn-new { padding: 7px 16px; background: var(--accent); color: var(--bg); border: none; border-radius: 8px; font-size: 13px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; }
|
||||
.btn-new:hover { background: var(--accent-hover); }
|
||||
.dark-toggle { width: 36px; height: 20px; background: var(--border); border-radius: 10px; position: relative; cursor: pointer; border: none; flex-shrink: 0; }
|
||||
.dark-toggle::after { content: '☀'; font-size: 12px; position: absolute; top: 2px; left: 3px; width: 16px; height: 16px; background: var(--bg-card); border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: left 0.2s; }
|
||||
.dark .dark-toggle::after { content: '☾'; left: 17px; }
|
||||
.avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; color: #fff; }
|
||||
.avatar.e { background: #5c3d2e; }
|
||||
.avatar.m { background: #b85c3a; }
|
||||
|
||||
/* Frise kanban */
|
||||
.frise { flex: 1; display: flex; gap: 0; overflow-x: auto; padding: 20px 16px; }
|
||||
.column { flex: 1; min-width: 220px; max-width: 320px; display: flex; flex-direction: column; }
|
||||
.column-header { font-family: 'Playfair Display', serif; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; padding: 10px 12px; text-align: center; border-bottom: 3px solid; margin-bottom: 8px; }
|
||||
.column[data-acte="Avant l'histoire"] .column-header { color: #6a7a8a; border-color: #6a7a8a; }
|
||||
.column[data-acte="Acte 1"] .column-header { color: #5c3d2e; border-color: #5c3d2e; }
|
||||
.column[data-acte="Acte 2"] .column-header { color: #b85c3a; border-color: #b85c3a; }
|
||||
.column[data-acte="Acte 3"] .column-header { color: #7a5c40; border-color: #7a5c40; }
|
||||
.column[data-acte="En attente"] .column-header { color: #a08a76; border-color: #e0d5c7; border-style: dashed; }
|
||||
.card-list { flex: 1; min-height: 60px; padding: 4px 6px; }
|
||||
/* ── Frise container ── */
|
||||
.frise-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
/* Timeline area */
|
||||
.timeline-area { flex: 1; overflow-x: auto; overflow-y: auto; padding: 20px 24px 12px; position: relative; }
|
||||
|
||||
/* Acte markers row */
|
||||
.acte-markers { display: flex; gap: 0; min-width: min-content; margin-bottom: 0; position: relative; }
|
||||
.acte-section { min-width: 200px; flex-shrink: 0; position: relative; }
|
||||
.acte-header { text-align: center; padding-bottom: 12px; }
|
||||
.acte-label { font-family: 'Playfair Display', serif; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.acte-section[data-acte="Avant l'histoire"] .acte-label { color: var(--blue-grey); }
|
||||
.acte-section[data-acte="Acte 1"] .acte-label { color: var(--brown); }
|
||||
.acte-section[data-acte="Acte 2"] .acte-label { color: var(--terra); }
|
||||
.acte-section[data-acte="Acte 3"] .acte-label { color: var(--olive); }
|
||||
|
||||
/* Timeline line */
|
||||
.timeline-line { height: 3px; background: var(--border); display: flex; min-width: min-content; border-radius: 2px; }
|
||||
.timeline-segment { flex-shrink: 0; height: 3px; }
|
||||
.acte-section[data-acte="Avant l'histoire"] .timeline-segment { background: var(--blue-grey); }
|
||||
.acte-section[data-acte="Acte 1"] .timeline-segment { background: var(--brown); }
|
||||
.acte-section[data-acte="Acte 2"] .timeline-segment { background: var(--terra); }
|
||||
.acte-section[data-acte="Acte 3"] .timeline-segment { background: var(--olive); }
|
||||
|
||||
/* Cards row under each acte */
|
||||
.cards-row { display: flex; gap: 10px; padding: 14px 8px 8px; min-height: 120px; align-items: flex-start; }
|
||||
|
||||
/* Cards */
|
||||
.card { background: #fff; border: 1px solid #e0d5c7; border-radius: 10px; padding: 14px 16px; margin-bottom: 8px; cursor: grab; border-left: 4px solid #e0d5c7; box-shadow: 0 1px 3px rgba(92,61,46,0.05); transition: box-shadow 0.15s; }
|
||||
.card:hover { box-shadow: 0 3px 12px rgba(92,61,46,0.1); }
|
||||
.card.sortable-ghost { opacity: 0.4; }
|
||||
.card.sortable-chosen { box-shadow: 0 4px 16px rgba(92,61,46,0.15); }
|
||||
.card-title { font-family: 'Playfair Display', serif; font-size: 14px; font-weight: 600; margin-bottom: 6px; }
|
||||
.card-summary { font-size: 12px; color: #6b5a4a; line-height: 1.4; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-meta { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.card-badge { font-size: 10px; padding: 2px 7px; border-radius: 8px; font-weight: 500; }
|
||||
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; cursor: grab; border-top: 3px solid var(--border); width: 170px; min-width: 170px; flex-shrink: 0; box-shadow: 0 1px 4px var(--shadow); transition: box-shadow 0.15s; }
|
||||
.card:hover { box-shadow: 0 4px 14px var(--shadow-hover); }
|
||||
.card.sortable-ghost { opacity: 0.3; }
|
||||
.card.sortable-chosen { box-shadow: 0 6px 20px var(--shadow-hover); }
|
||||
.card-title { font-family: 'Playfair Display', serif; font-size: 13px; font-weight: 600; margin-bottom: 5px; color: var(--text); }
|
||||
.card-summary { font-size: 11px; color: var(--text-muted); line-height: 1.4; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-meta { display: flex; gap: 4px; flex-wrap: wrap; }
|
||||
.card-badge { font-size: 9px; padding: 2px 6px; border-radius: 6px; font-weight: 500; }
|
||||
|
||||
/* Type colors on card border */
|
||||
.card[data-type="Scène"] { border-left-color: #5c3d2e; }
|
||||
.card[data-type="Événement"] { border-left-color: #b85c3a; }
|
||||
.card[data-type="Personnage"] { border-left-color: #7a8a5a; }
|
||||
.card[data-type="Lieu"] { border-left-color: #6a7a8a; }
|
||||
.card[data-type="Thème"] { border-left-color: #8a6a7a; }
|
||||
.card[data-type="Élément d'univers"] { border-left-color: #c49060; }
|
||||
/* Type colors on card top border */
|
||||
.card[data-type="Scène"] { border-top-color: var(--brown); }
|
||||
.card[data-type="Événement"] { border-top-color: var(--terra); }
|
||||
.card[data-type="Personnage"] { border-top-color: #7a8a5a; }
|
||||
.card[data-type="Lieu"] { border-top-color: var(--blue-grey); }
|
||||
.card[data-type="Thème"] { border-top-color: #8a6a7a; }
|
||||
.card[data-type="Élément d'univers"] { border-top-color: #c49060; }
|
||||
|
||||
/* Status badge colors */
|
||||
.badge-germe { background: #f0ebe3; color: #8a7968; }
|
||||
.badge-germe { background: var(--border); color: var(--text-muted); }
|
||||
.badge-etudier { background: #f5f0e4; color: #a07850; }
|
||||
.badge-posee { background: #eef3e8; color: #5a7a3a; }
|
||||
.badge-abandonnee { background: #f5f2ef; color: #a08a76; text-decoration: line-through; }
|
||||
.badge-abandonnee { background: var(--border); color: var(--text-hint); text-decoration: line-through; }
|
||||
.badge-conflit { background: #f5e4e4; color: #8b3030; }
|
||||
.dark .badge-etudier { background: #3a3020; color: #c49860; }
|
||||
.dark .badge-posee { background: #2a3520; color: #8aaa6a; }
|
||||
.dark .badge-conflit { background: #3a2020; color: #c06060; }
|
||||
.badge-perso { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); }
|
||||
|
||||
/* Personnage badges */
|
||||
.badge-perso { background: #faf6f0; color: #5c3d2e; border: 1px solid #e0d5c7; }
|
||||
/* ── Waiting zone (bottom) ── */
|
||||
.waiting-zone { border-top: 2px dashed var(--border); padding: 14px 24px; background: var(--bg-alt); flex-shrink: 0; }
|
||||
.waiting-header { font-size: 11px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 10px; font-weight: 600; }
|
||||
.waiting-cards { display: flex; gap: 10px; overflow-x: auto; min-height: 80px; padding: 4px 0; align-items: flex-start; }
|
||||
.waiting-cards .card { border-style: dashed; border-top: 3px dashed var(--border); }
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(44,24,16,0.4); z-index: 100; align-items: center; justify-content: center; }
|
||||
/* ── Modal ── */
|
||||
.modal-overlay { display: none; position: fixed; inset: 0; background: var(--overlay); z-index: 100; align-items: center; justify-content: center; }
|
||||
.modal-overlay.open { display: flex; }
|
||||
.modal { background: #fff; border-radius: 16px; width: 600px; max-width: 92vw; max-height: 88vh; overflow-y: auto; box-shadow: 0 12px 40px rgba(44,24,16,0.2); }
|
||||
.modal-header { padding: 24px 28px 0; display: flex; justify-content: space-between; align-items: start; }
|
||||
.modal-close { background: none; border: none; font-size: 22px; color: #a08a76; cursor: pointer; padding: 4px 8px; }
|
||||
.modal-close:hover { color: #5c3d2e; }
|
||||
.modal-body { padding: 16px 28px 28px; }
|
||||
.modal-body .field { margin-bottom: 18px; }
|
||||
.modal-body .field label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: #a08a76; font-weight: 600; margin-bottom: 6px; }
|
||||
.modal { background: var(--bg-card); border-radius: 16px; width: 580px; max-width: 92vw; max-height: 88vh; overflow-y: auto; box-shadow: 0 12px 40px var(--shadow-hover); }
|
||||
.modal-header { padding: 22px 26px 0; display: flex; justify-content: space-between; align-items: start; }
|
||||
.modal-close { background: none; border: none; font-size: 22px; color: var(--text-hint); cursor: pointer; padding: 4px 8px; }
|
||||
.modal-close:hover { color: var(--text); }
|
||||
.modal-body { padding: 14px 26px 26px; }
|
||||
.modal-body .field { margin-bottom: 16px; }
|
||||
.modal-body .field label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text-hint); font-weight: 600; margin-bottom: 5px; }
|
||||
.modal-body .field input,
|
||||
.modal-body .field textarea,
|
||||
.modal-body .field select { width: 100%; padding: 10px 14px; border: 1px solid #e0d5c7; border-radius: 8px; font-size: 14px; font-family: 'Lora', serif; color: #2c1810; outline: none; background: #faf6f0; }
|
||||
.modal-body .field select { width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; font-family: 'Lora', serif; color: var(--text); outline: none; background: var(--bg-input); }
|
||||
.modal-body .field input:focus,
|
||||
.modal-body .field textarea:focus,
|
||||
.modal-body .field select:focus { border-color: #8a7060; }
|
||||
.modal-body .field textarea { min-height: 80px; resize: vertical; line-height: 1.6; }
|
||||
.modal-body .field select:focus { border-color: var(--accent); }
|
||||
.modal-body .field textarea { min-height: 70px; resize: vertical; line-height: 1.6; }
|
||||
.modal-body .field-title input { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; border: none; background: transparent; padding: 0; }
|
||||
.modal-body .field-title input:focus { border-bottom: 2px solid #8a7060; }
|
||||
.modal-body .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||
.multi-select-container { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px; border: 1px solid #e0d5c7; border-radius: 8px; background: #faf6f0; min-height: 40px; }
|
||||
.multi-select-container .ms-tag { font-size: 12px; padding: 4px 10px; border-radius: 6px; background: #e8ddd0; color: #5c3d2e; cursor: pointer; }
|
||||
.multi-select-container .ms-tag.selected { background: #5c3d2e; color: #faf6f0; }
|
||||
.modal-body .field-title input:focus { border-bottom: 2px solid var(--accent); }
|
||||
.modal-body .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.multi-select-container { display: flex; flex-wrap: wrap; gap: 5px; padding: 8px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-input); min-height: 38px; }
|
||||
.multi-select-container .ms-tag { font-size: 12px; padding: 3px 9px; border-radius: 6px; background: var(--border); color: var(--text-muted); cursor: pointer; }
|
||||
.multi-select-container .ms-tag.selected { background: var(--accent); color: var(--bg); }
|
||||
.modal-actions { display: flex; gap: 10px; justify-content: space-between; margin-top: 8px; }
|
||||
.btn-save { padding: 10px 24px; background: #8a7060; color: #faf6f0; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; }
|
||||
.btn-save:hover { background: #5c3d2e; }
|
||||
.btn-delete { padding: 10px 16px; background: transparent; color: #8b3030; border: 1px solid #e8c4c0; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; cursor: pointer; }
|
||||
.btn-save { padding: 9px 22px; background: var(--accent); color: var(--bg); border: none; border-radius: 8px; font-size: 14px; font-weight: 600; font-family: 'Lora', serif; cursor: pointer; }
|
||||
.btn-save:hover { background: var(--accent-hover); }
|
||||
.btn-delete { padding: 9px 14px; background: transparent; color: #8b3030; border: 1px solid #e8c4c0; border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; cursor: pointer; }
|
||||
.btn-delete:hover { background: #fdf0ef; }
|
||||
.dark .btn-delete { border-color: #5a3030; }
|
||||
.dark .btn-delete:hover { background: #3a2020; }
|
||||
|
||||
/* Loading */
|
||||
.loading { text-align: center; padding: 60px; color: #a08a76; font-style: italic; }
|
||||
.loading { text-align: center; padding: 60px; color: var(--text-hint); font-style: italic; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
.frise { padding: 12px 8px; }
|
||||
.column { min-width: 180px; }
|
||||
.modal { width: 100%; max-width: 100%; border-radius: 12px 12px 0 0; max-height: 92vh; }
|
||||
.modal { width: 100%; max-width: 100%; border-radius: 12px 12px 0 0; }
|
||||
.card { width: 150px; min-width: 150px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -129,12 +202,19 @@
|
||||
<div class="topbar-title">Frise</div>
|
||||
<div class="topbar-right">
|
||||
<button class="btn-new" onclick="openNew()">+ Nouvelle idee</button>
|
||||
<button class="dark-toggle" onclick="toggleDark()" title="Mode sombre"></button>
|
||||
<div class="avatar e">E</div>
|
||||
<div class="avatar m">M</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frise" id="frise">
|
||||
<div class="loading" id="loading">Chargement...</div>
|
||||
<div class="frise-wrapper">
|
||||
<div class="timeline-area" id="timeline-area">
|
||||
<div class="loading" id="loading">Chargement...</div>
|
||||
</div>
|
||||
<div class="waiting-zone">
|
||||
<div class="waiting-header">En attente — glisser sur la frise</div>
|
||||
<div class="waiting-cards" id="waiting-cards"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +223,7 @@
|
||||
<div class="modal-overlay" id="modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span style="font-family:'Playfair Display',serif;font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#a08a76;" id="modal-label">Nouvelle idee</span>
|
||||
<span style="font-family:'Playfair Display',serif;font-size:12px;text-transform:uppercase;letter-spacing:1px;color:var(--text-hint);" id="modal-label">Nouvelle idee</span>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -199,93 +279,130 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const ACTES = ["Avant l'histoire", "Acte 1", "Acte 2", "Acte 3", "En attente"];
|
||||
const ACTES_FRISE = ["Avant l'histoire", "Acte 1", "Acte 2", "Acte 3"];
|
||||
let records = [];
|
||||
let columns = {};
|
||||
let editingId = null;
|
||||
|
||||
// ── Dark mode ──
|
||||
function toggleDark() {
|
||||
document.body.classList.toggle('dark');
|
||||
localStorage.setItem('tdb-dark', document.body.classList.contains('dark') ? '1' : '0');
|
||||
}
|
||||
if (localStorage.getItem('tdb-dark') === '1') document.body.classList.add('dark');
|
||||
|
||||
const statusClass = s => {
|
||||
if (!s) return '';
|
||||
const map = { 'Germe': 'badge-germe', 'À étudier': 'badge-etudier', 'Posée': 'badge-posee', 'Abandonnée': 'badge-abandonnee', 'En conflit': 'badge-conflit' };
|
||||
return map[s] || 'badge-germe';
|
||||
};
|
||||
|
||||
function makeCard(r) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.dataset.id = r.Id;
|
||||
card.dataset.type = r.Type || '';
|
||||
card.onclick = () => openEdit(r.Id);
|
||||
|
||||
const persos = r.Personnage ? r.Personnage.split(',').map(p =>
|
||||
`<span class="card-badge badge-perso">${esc(p.trim())}</span>`
|
||||
).join('') : '';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-title">${esc(r.Title || '')}</div>
|
||||
${r['Résumé'] ? `<div class="card-summary">${esc(r['Résumé'])}</div>` : ''}
|
||||
<div class="card-meta">
|
||||
${r.Statut ? `<span class="card-badge ${statusClass(r.Statut)}">${esc(r.Statut)}</span>` : ''}
|
||||
${r.Type ? `<span class="card-badge badge-perso">${esc(r.Type)}</span>` : ''}
|
||||
${persos}
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderFrise() {
|
||||
const frise = document.getElementById('frise');
|
||||
frise.innerHTML = '';
|
||||
const area = document.getElementById('timeline-area');
|
||||
area.innerHTML = '';
|
||||
|
||||
ACTES.forEach(acte => {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'column';
|
||||
col.dataset.acte = acte;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'column-header';
|
||||
header.textContent = acte;
|
||||
col.appendChild(header);
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'card-list';
|
||||
list.dataset.acte = acte;
|
||||
// Build horizontal timeline
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'display:flex; min-width:min-content;';
|
||||
|
||||
ACTES_FRISE.forEach(acte => {
|
||||
const acteRecords = records
|
||||
.filter(r => (r.Acte || 'En attente') === acte)
|
||||
.sort((a, b) => (a.Ordre || 0) - (b.Ordre || 0));
|
||||
|
||||
acteRecords.forEach(r => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.dataset.id = r.Id;
|
||||
card.dataset.type = r.Type || '';
|
||||
card.onclick = () => openEdit(r.Id);
|
||||
const section = document.createElement('div');
|
||||
section.className = 'acte-section';
|
||||
section.dataset.acte = acte;
|
||||
// Width adapts to content
|
||||
const minW = Math.max(200, acteRecords.length * 180 + 20);
|
||||
section.style.width = minW + 'px';
|
||||
|
||||
const persos = r.Personnage ? r.Personnage.split(',').map(p =>
|
||||
`<span class="card-badge badge-perso">${p.trim()}</span>`
|
||||
).join('') : '';
|
||||
section.innerHTML = `
|
||||
<div class="acte-header"><span class="acte-label">${esc(acte)}</span></div>
|
||||
<div class="timeline-segment" style="width:100%;height:3px;border-radius:2px;"></div>
|
||||
`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-title">${esc(r.Title || '')}</div>
|
||||
${r['Résumé'] ? `<div class="card-summary">${esc(r['Résumé'])}</div>` : ''}
|
||||
<div class="card-meta">
|
||||
${r.Statut ? `<span class="card-badge ${statusClass(r.Statut)}">${esc(r.Statut)}</span>` : ''}
|
||||
${r.Type ? `<span class="card-badge" style="background:#faf6f0;color:#5c3d2e;border:1px solid #e0d5c7;">${esc(r.Type)}</span>` : ''}
|
||||
${persos}
|
||||
</div>
|
||||
`;
|
||||
list.appendChild(card);
|
||||
});
|
||||
const row = document.createElement('div');
|
||||
row.className = 'cards-row';
|
||||
row.dataset.acte = acte;
|
||||
|
||||
col.appendChild(list);
|
||||
frise.appendChild(col);
|
||||
acteRecords.forEach(r => row.appendChild(makeCard(r)));
|
||||
|
||||
// SortableJS
|
||||
new Sortable(list, {
|
||||
section.appendChild(row);
|
||||
container.appendChild(section);
|
||||
|
||||
// SortableJS on horizontal row
|
||||
new Sortable(row, {
|
||||
group: 'frise',
|
||||
animation: 200,
|
||||
direction: 'horizontal',
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
onEnd: handleDrop,
|
||||
});
|
||||
});
|
||||
|
||||
area.appendChild(container);
|
||||
|
||||
// Waiting zone
|
||||
const waitingCards = document.getElementById('waiting-cards');
|
||||
waitingCards.innerHTML = '';
|
||||
const waitingRecords = records
|
||||
.filter(r => !r.Acte || r.Acte === 'En attente')
|
||||
.sort((a, b) => (a.Ordre || 0) - (b.Ordre || 0));
|
||||
|
||||
waitingRecords.forEach(r => waitingCards.appendChild(makeCard(r)));
|
||||
|
||||
new Sortable(waitingCards, {
|
||||
group: 'frise',
|
||||
animation: 200,
|
||||
direction: 'horizontal',
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
onEnd: handleDrop,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDrop(evt) {
|
||||
const cardEl = evt.item;
|
||||
const newActe = evt.to.dataset.acte;
|
||||
const cardId = Number(cardEl.dataset.id);
|
||||
const toContainer = evt.to;
|
||||
const newActe = toContainer.dataset.acte || 'En attente';
|
||||
|
||||
// Collect new order for all cards in the target column
|
||||
// Collect new order for target
|
||||
const updates = [];
|
||||
const cards = evt.to.querySelectorAll('.card');
|
||||
const cards = toContainer.querySelectorAll('.card');
|
||||
cards.forEach((c, i) => {
|
||||
updates.push({ id: Number(c.dataset.id), Acte: newActe, Ordre: i + 1 });
|
||||
});
|
||||
|
||||
// Also reorder source column if different
|
||||
// Reorder source if different
|
||||
if (evt.from !== evt.to) {
|
||||
const srcActe = evt.from.dataset.acte || 'En attente';
|
||||
const srcCards = evt.from.querySelectorAll('.card');
|
||||
srcCards.forEach((c, i) => {
|
||||
updates.push({ id: Number(c.dataset.id), Acte: evt.from.dataset.acte, Ordre: i + 1 });
|
||||
updates.push({ id: Number(c.dataset.id), Acte: srcActe, Ordre: i + 1 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,7 +412,6 @@ async function handleDrop(evt) {
|
||||
if (rec) { rec.Acte = u.Acte; rec.Ordre = u.Ordre; }
|
||||
});
|
||||
|
||||
// Persist
|
||||
await fetch('/api/reorder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -315,7 +431,6 @@ function populateSelects() {
|
||||
});
|
||||
});
|
||||
|
||||
// Multi-select personnages
|
||||
const container = document.getElementById('f-perso');
|
||||
container.innerHTML = '';
|
||||
if (columns.Personnage) {
|
||||
@@ -424,6 +539,7 @@ async function loadData() {
|
||||
fetch('/api/records'),
|
||||
fetch('/api/columns'),
|
||||
]);
|
||||
if (recRes.status === 401) { window.location.reload(); return; }
|
||||
const recData = await recRes.json();
|
||||
records = recData.list || [];
|
||||
columns = await colRes.json();
|
||||
@@ -437,17 +553,13 @@ function esc(s) {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// Close modal on overlay click
|
||||
document.getElementById('modal').addEventListener('click', e => {
|
||||
if (e.target === document.getElementById('modal')) closeModal();
|
||||
});
|
||||
|
||||
// Close modal on Escape
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
|
||||
// Init
|
||||
loadData();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user