Commit a33d353e authored by Kourser's avatar Kourser
Browse files

Mur de crise : thème clair, et habillage réel des plateformes

Le mur était figé en sombre : ses couleurs étaient codées en dur. Il porte
maintenant ses propres tokens (`--wall-*`) avec une variante claire, et un bouton
de bascule dans sa barre — un écran de salle ne se règle pas depuis la console.

Les tokens sont propres au mur plutôt qu'empruntés à la console : il est lu de loin
et doit rester contrasté indépendamment de la palette d'administration.

Le badge SIMULATION garde des couleurs fixes dans les deux thèmes. C'est un marqueur
de sûreté pour quiconque entre dans la salle : sa reconnaissance immédiate prime sur
l'harmonie visuelle.

Réseaux sociaux : le mur réutilise désormais `PostCard`, le composant du fil joueur,
au lieu d'un rendu maison en fiches. Le mur ressemble à la plateforme et non à un
tableau de bord — c'était le sens de la demande.

Pour rendre ça possible, les palettes de skin ont été séparées de la mise en page
des pages d'accueil : `.skin-x` / `.skin-li` / `.skin-ig` portent les couleurs,
`.x-home` et compagnie gardent leur grille. Aucune valeur dupliquée.

Conséquences assumées : les horodatages de presse passent à l'heure murale, comme
les publications, pour ne pas mélanger deux référentiels sur le même écran ; et le
badge « Cellule » disparaît du mur, puisque c'est un marqueur d'animation qui
n'aurait rien à faire sur un écran que les joueurs regardent aussi.

Les six règles CSS devenues mortes ont été retirées.

Non compilé ni exécuté (pas de toolchain Node dans l'environnement utilisé).

Co-Authored-By: Claude (RCA)
parent fb9e10c6
Loading
Loading
Loading
Loading
+27 −31
Original line number Diff line number Diff line
@@ -7,9 +7,11 @@ import type {
  WallData,
} from '@cythin/shared';
import { api, ApiError } from './api';
import { connectSocket, formatClock, RT } from './socket';
import { connectSocket, formatClock, formatWallTime, RT } from './socket';
import { outletClass, outletName } from './News';
import { PostCard } from './Social';
import { useT } from './i18n';
import { useTheme } from './theme';

/**
 * Mur de crise : page destinée à un écran partagé dans la salle de crise.
@@ -32,14 +34,20 @@ interface WallModule {
  label: string;
  /** Suffixe de classe portant l'identité visuelle de la source. */
  tone: string;
  /**
   * Classes reprenant l'habillage joueur de la plateforme : `skin-*` porte sa
   * palette, `*-feed` ses règles de carte. Absent pour la presse, qui a son
   * propre traitement de bandeau-titre.
   */
  skin?: string;
}

// Le flux TV viendra s'ajouter ici : une entrée, plus un cas dans `WallColumn`.
const MODULES: WallModule[] = [
  { key: 'NEWS', label: 'Presse', tone: 'news' },
  { key: 'X', label: 'X', tone: 'x' },
  { key: 'LINKEDIN', label: 'LinkedIn', tone: 'li' },
  { key: 'INSTAGRAM', label: 'Instagram', tone: 'ig' },
  { key: 'X', label: 'X', tone: 'x', skin: 'skin-x x-feed' },
  { key: 'LINKEDIN', label: 'LinkedIn', tone: 'li', skin: 'skin-li li-feed' },
  { key: 'INSTAGRAM', label: 'Instagram', tone: 'ig', skin: 'skin-ig ig-feed' },
];

const DEFAULT_ENABLED: WallModuleKey[] = ['NEWS', 'X'];
@@ -150,6 +158,7 @@ function Wall({
  load: () => Promise<WallData>;
}): JSX.Element {
  const { t } = useT();
  const { theme, toggle } = useTheme();
  const [enabled, setEnabled] = useState<WallModuleKey[]>(() => loadEnabled(exerciseId));
  /** Données du mur, avec l'instant de réception (pour extrapoler l'horloge). */
  const [snapshot, setSnapshot] = useState<{ data: WallData; at: number } | null>(null);
@@ -247,6 +256,14 @@ function Wall({
            </button>
          ))}
        </div>
        <button
          className="wall-fs"
          title={theme === 'dark' ? t('Passer en clair') : t('Passer en sombre')}
          aria-label={t('Changer de thème')}
          onClick={toggle}
        >
          {theme === 'dark' ? '' : ''}
        </button>
        <button
          className="wall-fs"
          title={t('Plein écran')}
@@ -274,7 +291,7 @@ function Wall({
                <h2 className="wall-col-head">
                  {t(m.label)} <span className="wall-count">{count}</span>
                </h2>
                <div className="wall-col-body">
                <div className={`wall-col-body ${m.skin ?? ''}`}>
                  <WallColumn module={m} news={news} posts={posts} />
                </div>
              </section>
@@ -306,7 +323,7 @@ function WallColumn({
          <article key={a.id} className={`wall-item wall-article ${outletClass(a.outlet)}`}>
            <div className="wall-item-top">
              <span className="wall-outlet">{outletName(a.outlet)}</span>
              <span className="wall-time mono">T+{formatClock(a.atExerciseSec)}</span>
              <span className="wall-time mono">{formatWallTime(a.createdAt)}</span>
            </div>
            <h3 className="wall-headline">{a.headline}</h3>
            <div className="wall-meta">
@@ -322,35 +339,14 @@ function WallColumn({

  const feed = posts.filter((p) => p.network === module.key);
  if (feed.length === 0) return <p className="wall-none">{t('Aucune publication.')}</p>;
  // Rendu identique a celui des joueurs : le mur doit ressembler a la plateforme,
  // pas a un tableau de bord. L'animation d'arrivee est portee par le conteneur.
  return (
    <>
      {feed.map((p) => (
        <article key={p.id} className="wall-item wall-post">
          <div className="wall-item-top">
            <img className="wall-av" src={p.avatarUrl} alt="" loading="lazy" />
            <span className="wall-author">{p.authorName}</span>
            <span className="wall-handle">@{p.authorHandle.replace(/^@+/, '')}</span>
            {p.authorParticipantId && <span className="wall-tag">{t('Cellule')}</span>}
            <span className="wall-time mono">T+{formatClock(p.atExerciseSec)}</span>
        <div key={p.id} className="wall-feed-item">
          <PostCard p={p} />
        </div>
          <p className="wall-body">{p.body}</p>
          <div className="wall-stats">
            <span>{p.likes}</span>
            <span>🔁 {p.shares}</span>
            <span>💬 {p.comments + p.replies.length}</span>
          </div>
          {p.replies.length > 0 && (
            <div className="wall-replies">
              {p.replies.map((r) => (
                <div key={r.id} className="wall-reply">
                  <span className="wall-author">{r.authorName}</span>{' '}
                  <span className="wall-handle">@{r.authorHandle.replace(/^@+/, '')}</span>
                  <div>{r.body}</div>
                </div>
              ))}
            </div>
          )}
        </article>
      ))}
    </>
  );
+6 −2
Original line number Diff line number Diff line
@@ -420,8 +420,12 @@ function ReplyThread({ p }: { p: ApiSocialPost }): JSX.Element | null {
  );
}

/** Carte d'un post, stylee selon le reseau (tweet, publication LinkedIn ou post Instagram). */
function PostCard({ p }: { p: ApiSocialPost }): JSX.Element {
/**
 * Carte d'un post, stylee selon le reseau (tweet, publication LinkedIn ou post
 * Instagram). Exportee pour que le mur de crise affiche exactement le meme rendu
 * que les joueurs — c'est ce qui rend le mur credible.
 */
export function PostCard({ p }: { p: ApiSocialPost }): JSX.Element {
  const { t } = useT();
  const handle = p.authorHandle.replace(/^@+/, '');
  // L'engagement affiche cumule le socle factice de l'animation et les vraies reponses.
+102 −64
Original line number Diff line number Diff line
@@ -1747,7 +1747,12 @@ textarea {

/* ---------- X (Twitter) ---------- */
/* Habillage pilote par variables : sombre par defaut, clair sous data-theme=light. */
.x-home {
/*
 * Palette separee de la mise en page : le mur de crise reutilise l'habillage des
 * plateformes sans reprendre leur page d'accueil (`.skin-*` porte les couleurs).
 */
.x-home,
.skin-x {
  --x-bg: #000;
  --x-fg: #e7e9ea;
  --x-border: #2f3336;
@@ -1757,7 +1762,9 @@ textarea {
  --x-search: #202327;
  --x-post-bg: #eff3f4;
  --x-post-fg: #0f1419;
}

.x-home {
  display: grid;
  grid-template-columns: 260px minmax(0, 600px) 330px;
  justify-content: center;
@@ -1769,7 +1776,8 @@ textarea {
  min-height: 640px;
}

:root[data-theme='light'] .x-home {
:root[data-theme='light'] .x-home,
:root[data-theme='light'] .skin-x {
  --x-bg: #ffffff;
  --x-fg: #0f1419;
  --x-border: #eff3f4;
@@ -2051,7 +2059,8 @@ textarea {

/* ---------- LinkedIn ---------- */
/* Habillage pilote par variables : clair par defaut, sombre sous data-theme=dark. */
.li-home {
.li-home,
.skin-li {
  --li-bg: #f4f2ee;
  --li-card: #ffffff;
  --li-border: #e8e8e8;
@@ -2059,7 +2068,9 @@ textarea {
  --li-muted: #00000099;
  --li-nav: #ffffff;
  --li-search: #edf3f8;
}

.li-home {
  background: var(--li-bg);
  border-radius: 14px;
  overflow: hidden;
@@ -2067,7 +2078,8 @@ textarea {
  color: var(--li-fg);
}

:root[data-theme='dark'] .li-home {
:root[data-theme='dark'] .li-home,
:root[data-theme='dark'] .skin-li {
  --li-bg: #12161a;
  --li-card: #1b2127;
  --li-border: #38434f;
@@ -4390,31 +4402,58 @@ textarea {
  font-size: 13px;
}

/* Le mur impose son propre fond : il est lu de loin, jamais de près. */
/*
 * Le mur porte ses propres tokens : il est lu de loin et doit rester contrasté
 * dans les deux thèmes, indépendamment de la palette de la console.
 */
.crisis-wall {
  --wall-bg: #0a0e13;
  --wall-bar: #05080b;
  --wall-panel: #101720;
  --wall-ink: #e8eef5;
  --wall-muted: #8695a6;
  --wall-faint: #5c6b7a;
  --wall-border: #1e2732;
  --wall-line: #2b3644;

  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: #0a0e13;
  color: #e8eef5;
  background: var(--wall-bg);
  color: var(--wall-ink);
  overflow: hidden;
}

:root[data-theme='light'] .crisis-wall {
  --wall-bg: #eef1f6;
  --wall-bar: #ffffff;
  --wall-panel: #ffffff;
  --wall-ink: #1a2029;
  --wall-muted: #5c6b7a;
  --wall-faint: #8695a6;
  --wall-border: #dde3ec;
  --wall-line: #c6cfdb;
}
.wall-bar {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 8px 16px;
  background: #05080b;
  border-bottom: 1px solid #1e2732;
  background: var(--wall-bar);
  border-bottom: 1px solid var(--wall-border);
}
/*
 * Le badge SIMULATION garde des couleurs fixes dans les deux themes : c'est un
 * marqueur de surete, sa reconnaissance immediate prime sur l'harmonie visuelle.
 */
.wall-sim {
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.12em;
  color: #0a0e13;
  background: var(--warn);
  color: #241b06;
  background: #f0b64d;
  border-radius: 4px;
  padding: 3px 9px;
}
@@ -4439,19 +4478,19 @@ textarea {
  flex-wrap: wrap;
}
.wall-chip {
  border: 1px solid #2b3644;
  border: 1px solid var(--wall-line);
  background: transparent;
  color: #8695a6;
  color: var(--wall-muted);
  border-radius: 20px;
  padding: 4px 13px;
  font-size: 13px;
  cursor: pointer;
}
.wall-chip.on {
  color: #0a0e13;
  color: var(--wall-bg);
  font-weight: 650;
  background: #8695a6;
  border-color: #8695a6;
  background: var(--wall-muted);
  border-color: var(--wall-muted);
}
.wall-chip.tone-news.on {
  background: #d5382f;
@@ -4474,9 +4513,9 @@ textarea {
  color: #fff;
}
.wall-fs {
  border: 1px solid #2b3644;
  border: 1px solid var(--wall-line);
  background: transparent;
  color: #8695a6;
  color: var(--wall-muted);
  border-radius: 8px;
  width: 32px;
  height: 32px;
@@ -4489,7 +4528,7 @@ textarea {
  flex: 1;
  display: grid;
  place-items: center;
  color: #8695a6;
  color: var(--wall-muted);
  font-size: 20px;
}

@@ -4498,7 +4537,7 @@ textarea {
  flex: 1;
  display: flex;
  gap: 1px;
  background: #1e2732;
  background: var(--wall-border);
  min-height: 0;
}
.wall-col {
@@ -4506,7 +4545,7 @@ textarea {
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: #0a0e13;
  background: var(--wall-bg);
}
.wall-col-head {
  margin: 0;
@@ -4514,7 +4553,7 @@ textarea {
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-bottom: 2px solid #2b3644;
  border-bottom: 2px solid var(--wall-line);
}
.wall-col.tone-news .wall-col-head {
  border-bottom-color: #d5382f;
@@ -4529,7 +4568,7 @@ textarea {
  border-bottom-color: #d6249f;
}
.wall-count {
  color: #8695a6;
  color: var(--wall-muted);
  font-weight: 500;
}
.wall-col-body {
@@ -4541,7 +4580,7 @@ textarea {
  gap: 12px;
}
.wall-none {
  color: #5c6b7a;
  color: var(--wall-faint);
}

/* Un élément qui arrive se signale : seuls les nouveaux nœuds sont montés. */
@@ -4557,10 +4596,10 @@ textarea {
}
.wall-item {
  animation: wall-in 0.45s ease-out;
  border: 1px solid #1e2732;
  border: 1px solid var(--wall-border);
  border-radius: 10px;
  padding: 12px 14px;
  background: #101720;
  background: var(--wall-panel);
}
.wall-item-top {
  display: flex;
@@ -4575,7 +4614,7 @@ textarea {
}
.wall-time {
  margin-left: auto;
  color: #8695a6;
  color: var(--wall-muted);
  font-size: 13px;
}
.wall-headline {
@@ -4594,36 +4633,6 @@ textarea {
  line-height: 1.5;
  white-space: pre-wrap;
}
.wall-author {
  font-weight: 700;
}
.wall-handle {
  color: #8695a6;
}
.wall-tag {
  font-size: 11px;
  font-weight: 650;
  color: var(--brand);
  border: 1px solid var(--brand);
  border-radius: 20px;
  padding: 1px 8px;
}
.wall-stats {
  display: flex;
  gap: 16px;
  margin-top: 8px;
  color: #8695a6;
  font-size: 14px;
}
.wall-replies {
  margin-top: 10px;
  padding-left: 12px;
  border-left: 2px solid #2b3644;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 14px;
}

/* Écran portrait ou fenêtre étroite : les colonnes s'empilent. */
@media (max-width: 900px) {
@@ -4884,13 +4893,6 @@ textarea {
}

/* Mur de crise : avatar de l'auteur d'une publication. */
.wall-av {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
}

/* Canal de chat porteur de messages non lus */
.chan.has-unread {
@@ -5061,3 +5063,39 @@ textarea {
  color: var(--down);
  background: var(--panel-2);
}

/* ---- Mur de crise : colonnes sociales à l'habillage des plateformes ---- */
/*
 * Instagram n'expose pas de variables : on reprend son bicolore explicite, comme
 * `.ig-home`, pour que la colonne du mur ait le même fond que l'espace joueur.
 */
.skin-ig {
  background: #fff;
  color: #000;
}
:root[data-theme='dark'] .skin-ig {
  background: #000;
  color: #f5f5f5;
}
.skin-x {
  background: var(--x-bg);
  color: var(--x-fg);
}
.skin-li {
  background: var(--li-bg);
  color: var(--li-fg);
}

/* L'animation d'arrivée passe du `.wall-item` au conteneur de la publication. */
.wall-feed-item {
  animation: wall-in 0.45s ease-out;
}

/* Le mur affiche les cartes en pleine largeur de colonne, sans grille d'accueil. */
.wall-col-body.x-feed,
.wall-col-body.li-feed,
.wall-col-body.ig-feed {
  display: flex;
  flex-direction: column;
  gap: 12px;
}