33800 Docs

← Retour

Proposition : Composant Header Universel + Liste Modules

Status : IMPLEMENTEE

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ LAYOUT : TABS (reste dans le layout)                            │
│  [←] [📄 Contenu] [⚙️ Paramètres] [🔍 SEO] [📑 Sous-pages]      │
├──────────────────────────────────────────────────────────────────┤
│ PAGE ENFANT : AdminPageHeader (dans chaque page)                │
│                                                                  │
│  Titre de section                              [+ Action]        │
│  Description optionnelle                                        │
├──────────────────────────────────────────────────────────────────┤
│ Stats (optionnel)                                               │
├──────────────────────────────────────────────────────────────────┤
│ Contenu (liste, form, etc.)                                     │
└──────────────────────────────────────────────────────────────────┘

1. Layout page edit (simplifié, garde les tabs)

// routes/admin/(pages)/page/[pageId]/layout.tsx

export default component$(() => {
  const location = useLocation();
  const store = useStore({
    pageId: null as string | null,
    pageTitle: '' as string,
    backUrl: '/admin/pages' as string,
    tabs: [] as Array<{ id: string; label: string; url: string; icon: string }>,
    currentTab: 'content',
  });

  useTask$(async ({ track }) => {
    const pageId = track(() => location.params.pageId);
    if (!pageId) return;

    const pageData = await loadPageData(pageId);
    const { meta, parentPage } = pageData;

    store.pageId = meta.id;
    store.pageTitle = meta.title;
    store.backUrl = parentPage 
      ? `/admin/page/${parentPage.meta.id}/subpages`
      : '/admin/pages';

    store.tabs = [
      { id: 'content', label: 'Contenu', url: `/admin/page/${meta.id}/content`, icon: '📄' },
      { id: 'settings', label: 'Paramètres', url: `/admin/page/${meta.id}/settings`, icon: '⚙️' },
      { id: 'seo', label: 'SEO', url: `/admin/page/${meta.id}/seo`, icon: '🔍' },
      { id: 'subpages', label: 'Sous-pages', url: `/admin/page/${meta.id}/subpages`, icon: '📑' },
    ];
    store.currentTab = store.tabs.find(t => location.url.pathname.includes(t.id))?.id || 'content';
  });

  return (
    <div class="admin-page-edit">
      {/* TABS uniquement - reste dans le layout */}
      <nav class="admin-tabs admin-tabs--full">
        <Link href={store.backUrl} class="admin-tabs__item admin-tabs__item--back">
          <span class="admin-tabs__icon">←</span>
        </Link>
        {store.tabs.map((tab) => (
          <Link
            key={tab.id}
            href={tab.url}
            class={`admin-tabs__item ${store.currentTab === tab.id ? 'admin-tabs__item--active' : ''}`}
          >
            <span class="admin-tabs__icon">{tab.icon}</span>
            <span class="admin-tabs__label">{tab.label}</span>
          </Link>
        ))}
      </nav>

      {/* Contenu - chaque page gère son propre header */}
      <main class="admin-page-edit__content">
        <Slot />
      </main>
    </div>
  );
});

2. Composant AdminPageHeader (sans tabs)

// src/components/admin/ui/index.tsx

interface AdminPageHeaderProps {
  title: string;
  subtitle?: string;
  icon?: string;
  backHref?: string;
  backLabel?: string;
  badge?: { text: string; variant?: 'success' | 'warning' | 'danger' | 'info' | 'neutral' };
  parentInfo?: { label: string; href?: string };
}

export const AdminPageHeader = component$<AdminPageHeaderProps>((props) => {
  const { title, subtitle, icon, backHref, backLabel, badge, parentInfo } = props;

  return (
    <div class="admin-page-header">
      <div class="admin-page-header__main">
        <div class="admin-page-header__left">
          {backHref && (
            <Link href={backHref} class="admin-page-header__back" title={backLabel || 'Retour'}>
              ←
            </Link>
          )}

          <div class="admin-page-header__title-block">
            <div class="admin-page-header__title-row">
              {icon && <span class="admin-page-header__icon">{icon}</span>}
              <h1 class="admin-page-header__title">{title}</h1>
              {badge && (
                <span class={`admin-badge admin-badge--${badge.variant || 'neutral'}`}>
                  {badge.text}
                </span>
              )}
              {parentInfo && (
                <span class="admin-page-header__parent">
                  ({parentInfo.href ? (
                    <Link href={parentInfo.href}>{parentInfo.label}</Link>
                  ) : parentInfo.label})
                </span>
              )}
            </div>

            {subtitle && (
              <p class="admin-page-header__subtitle">{subtitle}</p>
            )}
          </div>
        </div>

        <div class="admin-page-header__actions">
          <Slot />
        </div>
      </div>
    </div>
  );
});

3. Exemples d'utilisation

Page Content (sous les tabs du layout)

export default component$(() => {
  const pageData = usePageContentData();
  const { meta, content } = pageData.value;
  const modules = useSignal(content.modules);

  return (
    <div class="admin-section">
      <AdminPageHeader 
        title="Modules" 
        subtitle="Gérez les modules de contenu de cette page"
      >
        <AdminButton variant="primary" href={`/admin/page/${meta.id}/modules/add`}>
          + Ajouter un module
        </AdminButton>
      </AdminPageHeader>

      <AdminStats>
        <AdminStat value={modules.value.length} label="Total" />
        <AdminStat value={...} label="Standards" />
        <AdminStat value={...} label="Communs" />
      </AdminStats>

      {/* Liste modules */}
    </div>
  );
});

Page Settings (sous les tabs du layout)

export default component$(() => {
  return (
    <div class="admin-section">
      <AdminPageHeader 
        title="Paramètres" 
        subtitle="Configurez les options de cette page"
      >
        <AdminButton variant="primary" type="submit" form="settings-form">
          Sauvegarder
        </AdminButton>
      </AdminPageHeader>

      <form id="settings-form">
        {/* Formulaire */}
      </form>
    </div>
  );
});

Dashboard (page principale, sans tabs)

<AdminPageHeader 
  title="Dashboard" 
  subtitle={`Bienvenue sur ${siteName}`}
>
  <AdminButton variant="primary" href="/admin/newPage">
    + Nouvelle page
  </AdminButton>
</AdminPageHeader>

Config Colors (sans tabs)

<AdminPageHeader 
  icon="🎨"
  title="Couleurs" 
  subtitle="Configurez la palette de couleurs"
>
  <AdminButton variant="primary" href="/admin/config/colors/new">
    + Nouvelle couleur
  </AdminButton>
</AdminPageHeader>

4. CSS

.admin-page-header {
  margin-bottom: var(--space-lg);
}

.admin-page-header__main {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-lg);
  flex-wrap: wrap;
}

.admin-page-header__left {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
}

.admin-page-header__back {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  background: var(--surface-hover);
  color: var(--text-primary);
  text-decoration: none;
  font-size: 1.2rem;
}

.admin-page-header__title-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

.admin-page-header__icon {
  font-size: 1.25rem;
}

.admin-page-header__title {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 0;
}

.admin-page-header__parent {
  font-size: 0.875rem;
  color: var(--text-muted);
}

.admin-page-header__subtitle {
  margin: var(--space-xs) 0 0;
  color: var(--text-muted);
  font-size: 0.875rem;
}

.admin-page-header__actions {
  display: flex;
  gap: var(--space-sm);
}

5. Liste Modules (ModuleListItem)

Style liste comme pages :

// src/components/admin/module-list-item/index.tsx

export const ModuleListItem = component$<ModuleListItemProps>((props) => {
  const { module, position, pageId, isFirst, isLast } = props;
  const isCommonRef = module.type === 'common-module-ref';

  return (
    <div class="admin-list-item admin-list-item--with-position">
      <div class="admin-list-item__position-col">
        <span class="admin-list-item__position">{position + 1}</span>
        <div class="admin-list-item__reorder">
          <button disabled={isFirst} onClick$={props.onMoveUp$}>↑</button>
          <button disabled={isLast} onClick$={props.onMoveDown$}>↓</button>
        </div>
      </div>

      <span class="admin-list-item__icon">{isCommonRef ? '🔗' : '📦'}</span>

      <div class="admin-list-item__content">
        <div class="admin-list-item__title">
          {module.type}
          {isCommonRef && <span class="admin-badge admin-badge--info">Commun</span>}
        </div>
        <div class="admin-list-item__meta">
          Template: {module.template} • ID: {module.id.slice(0, 8)}...
        </div>
      </div>

      <div class="admin-list-item__actions">
        <AdminButton variant="ghost" size="sm" href={...}>✏️</AdminButton>
        <AdminButton variant="ghost" size="sm" onClick$={props.onDuplicate$}>📋</AdminButton>
        <AdminButton variant="ghost" size="sm" onClick$={props.onMove$}>🔄</AdminButton>
        <AdminButton variant="ghost" size="sm" onClick$={props.onDelete$}>🗑️</AdminButton>
      </div>
    </div>
  );
});

6. Fichiers impactés

Fichier Action
components/admin/ui/index.tsx Refacto AdminPageHeader
components/admin/module-list-item/index.tsx CRÉER
routes/admin/(pages)/page/[pageId]/layout.tsx Simplifier (retirer header, garder tabs)
routes/admin/(pages)/page/[pageId]/content/index.tsx Ajouter AdminPageHeader
routes/admin/(pages)/page/[pageId]/settings/index.tsx Ajouter AdminPageHeader
routes/admin/(pages)/page/[pageId]/seo/index.tsx Ajouter AdminPageHeader
routes/admin/(pages)/page/[pageId]/subpages/index.tsx Ajouter AdminPageHeader
Toutes pages config/* Migrer vers AdminPageHeader

Validation