Status : IMPLEMENTEE
┌─────────────────────────────────────────────────────────────────┐
│ 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.) │
└──────────────────────────────────────────────────────────────────┘
// 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>
);
});
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>
);
});
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>
);
});
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>
);
});
<AdminPageHeader
title="Dashboard"
subtitle={`Bienvenue sur ${siteName}`}
>
<AdminButton variant="primary" href="/admin/newPage">
+ Nouvelle page
</AdminButton>
</AdminPageHeader>
<AdminPageHeader
icon="🎨"
title="Couleurs"
subtitle="Configurez la palette de couleurs"
>
<AdminButton variant="primary" href="/admin/config/colors/new">
+ Nouvelle couleur
</AdminButton>
</AdminPageHeader>
.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);
}
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>
);
});
| 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 |