Compare commits

...

2 Commits

Author SHA1 Message Date
hykocx 50f04f762b chore: bump version to 1.4.96 2026-04-24 20:27:33 -04:00
hykocx 970092fccb feat(admin): add devkit developer tools section
- add `ZEN_DEVKIT` env variable to enable/disable devkit
- add `isDevkitEnabled()` utility and export it from public api
- register devkit nav section and items conditionally when devkit is enabled
- add devkit route handling in admin page client and server
- add DevkitPage, ComponentsPage, and IconsPage client components
2026-04-24 20:27:30 -04:00
11 changed files with 296 additions and 6 deletions
+3
View File
@@ -50,3 +50,6 @@ ZEN_PUBLIC_LOGO_URL=
# OTHERS
NEXT_TELEMETRY_DISABLED=1
# DEVKIT (developer tools)
ZEN_DEVKIT=false
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@zen/core",
"version": "1.4.95",
"version": "1.4.96",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@zen/core",
"version": "1.4.95",
"version": "1.4.96",
"license": "GPL-3.0-only",
"dependencies": {
"@headlessui/react": "^2.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@zen/core",
"version": "1.4.95",
"version": "1.4.96",
"description": "Une plateforme multi-usage construite sur l'essentiel, rien de plus, rien de moins.",
"repository": {
"type": "git",
+5 -1
View File
@@ -8,8 +8,9 @@ import './pages/ProfilePage.client.js';
import './pages/SettingsPage.client.js';
import './pages/ConfirmEmailChangePage.client.js';
import './widgets/index.client.js';
import './devkit/DevkitPage.client.js';
export default function AdminPageClient({ params, user, widgetData, appConfig }) {
export default function AdminPageClient({ params, user, widgetData, appConfig, devkitEnabled }) {
const parts = params?.admin || [];
const [first] = parts;
@@ -25,5 +26,8 @@ export default function AdminPageClient({ params, user, widgetData, appConfig })
if (slug === 'settings') {
return <Component user={user} appConfig={appConfig} />;
}
if (slug === 'devkit') {
return <Component user={user} params={parts} devkitEnabled={devkitEnabled} />;
}
return <Component user={user} params={parts} />;
}
+3
View File
@@ -2,12 +2,14 @@ import AdminPageClient from './AdminPage.client.js';
import { protectAdmin } from './protect.js';
import { collectWidgetData } from './registry.js';
import { getAppConfig, getPublicBaseUrl } from '@zen/core';
import { isDevkitEnabled } from '../../shared/lib/appConfig.js';
export default async function AdminPage({ params }) {
const resolvedParams = await params;
const session = await protectAdmin();
const widgetData = await collectWidgetData();
const appConfig = { ...getAppConfig(), siteUrl: getPublicBaseUrl() };
const devkitEnabled = isDevkitEnabled();
return (
<AdminPageClient
@@ -15,6 +17,7 @@ export default async function AdminPage({ params }) {
user={session.user}
widgetData={widgetData}
appConfig={appConfig}
devkitEnabled={devkitEnabled}
/>
);
}
@@ -0,0 +1,173 @@
'use client';
import {
Button,
Card,
Badge,
StatusBadge,
Input,
Select,
Textarea,
Switch,
StatCard,
Loading,
} from '@zen/core/shared/components';
import { UserCircle02Icon } from '@zen/core/shared/icons';
import AdminHeader from '../components/AdminHeader.js';
function PreviewBlock({ title, children }) {
return (
<div className="flex flex-col gap-3">
<h3 className="text-[11px] font-semibold uppercase tracking-widest text-neutral-500 dark:text-neutral-400">{title}</h3>
<div className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900/50 p-6 flex flex-wrap gap-3 items-center">
{children}
</div>
</div>
);
}
export default function ComponentsPage() {
return (
<div className="flex flex-col gap-8">
<AdminHeader title="Composants" description="Catalogue visuel des composants partagés" />
<PreviewBlock title="Button — variants">
<Button variant="primary" size="md">Primary</Button>
<Button variant="secondary" size="md">Secondary</Button>
<Button variant="success" size="md">Success</Button>
<Button variant="danger" size="md">Danger</Button>
<Button variant="warning" size="md">Warning</Button>
<Button variant="ghost" size="md">Ghost</Button>
<Button variant="fullghost" size="md">Full Ghost</Button>
</PreviewBlock>
<PreviewBlock title="Button — tailles">
<Button variant="primary" size="sm">Small</Button>
<Button variant="primary" size="md">Medium</Button>
<Button variant="primary" size="lg">Large</Button>
</PreviewBlock>
<PreviewBlock title="Button — états">
<Button variant="primary" disabled>Désactivé</Button>
<Button variant="primary" loading>Chargement</Button>
</PreviewBlock>
<PreviewBlock title="Badge — variants">
<Badge variant="default">Default</Badge>
<Badge variant="primary">Primary</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="danger">Danger</Badge>
<Badge variant="info">Info</Badge>
<Badge variant="purple">Purple</Badge>
<Badge variant="pink">Pink</Badge>
<Badge variant="orange">Orange</Badge>
</PreviewBlock>
<PreviewBlock title="StatusBadge">
<StatusBadge status="active" />
<StatusBadge status="inactive" />
<StatusBadge status="pending" />
<StatusBadge status="verified" />
<StatusBadge status="unverified" />
<StatusBadge status="admin" />
<StatusBadge status="user" />
</PreviewBlock>
<PreviewBlock title="Card — variants">
{['default', 'elevated', 'outline', 'success', 'info', 'warning', 'danger'].map(v => (
<Card key={v} variant={v} padding="md" className="min-w-[140px]">
<span className="text-sm font-medium">{v}</span>
</Card>
))}
</PreviewBlock>
<PreviewBlock title="StatCard">
<StatCard
title="Utilisateurs"
value="1 234"
change="+42 ce mois"
changeType="increase"
icon={UserCircle02Icon}
color="text-blue-700"
bgColor="bg-blue-700/10"
className="w-56"
/>
<StatCard
title="Revenus"
value="8 400 $"
change="-120 ce mois"
changeType="decrease"
icon={UserCircle02Icon}
color="text-red-700"
bgColor="bg-red-700/10"
className="w-56"
/>
<StatCard
title="Chargement"
value="..."
icon={UserCircle02Icon}
color="text-neutral-400"
bgColor="bg-neutral-400/10"
loading
className="w-56"
/>
</PreviewBlock>
<PreviewBlock title="Input">
<div className="w-72 flex flex-col gap-3">
<Input label="Champ normal" placeholder="Valeur..." value="" onChange={() => {}} />
<Input label="Avec description" placeholder="Valeur..." value="" description="Texte d'aide sous le champ." onChange={() => {}} />
<Input label="Avec erreur" placeholder="Valeur..." value="" error="Ce champ est invalide." onChange={() => {}} />
<Input label="Désactivé" placeholder="Valeur..." value="Valeur fixe" disabled onChange={() => {}} />
<Input label="Requis" placeholder="Valeur..." value="" required onChange={() => {}} />
</div>
</PreviewBlock>
<PreviewBlock title="Select">
<div className="w-72 flex flex-col gap-3">
<Select
label="Sélection normale"
value="option1"
options={[{ value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }]}
onChange={() => {}}
/>
<Select
label="Avec erreur"
value=""
options={[{ value: 'option1', label: 'Option 1' }]}
error="Veuillez choisir une option."
onChange={() => {}}
/>
<Select
label="Désactivé"
value="option1"
options={[{ value: 'option1', label: 'Option 1' }]}
disabled
onChange={() => {}}
/>
</div>
</PreviewBlock>
<PreviewBlock title="Textarea">
<div className="w-72 flex flex-col gap-3">
<Textarea label="Zone de texte" placeholder="Entrer du texte..." value="" rows={3} onChange={() => {}} />
<Textarea label="Avec erreur" placeholder="..." value="" error="Ce champ est requis." rows={2} onChange={() => {}} />
<Textarea label="Désactivé" value="Texte fixe" disabled rows={2} onChange={() => {}} />
</div>
</PreviewBlock>
<PreviewBlock title="Switch">
<div className="w-72 flex flex-col gap-1 border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden">
<Switch label="Désactivé" description="Ce switch est off" checked={false} onChange={() => {}} />
<Switch label="Activé" description="Ce switch est on" checked={true} onChange={() => {}} />
<Switch label="Désactivé (disabled)" checked={false} disabled onChange={() => {}} />
</div>
</PreviewBlock>
<PreviewBlock title="Loading">
<Loading />
</PreviewBlock>
</div>
);
}
@@ -0,0 +1,23 @@
'use client';
import { registerPage } from '../registry.js';
import ComponentsPage from './ComponentsPage.client.js';
import IconsPage from './IconsPage.client.js';
function DevkitPage({ params, devkitEnabled }) {
if (!devkitEnabled) {
return (
<div className="flex items-center justify-center py-24 text-neutral-400 dark:text-neutral-600 text-sm">
DevKit désactivé. Définir <code className="mx-1 font-mono bg-neutral-100 dark:bg-neutral-800 px-1 rounded">ZEN_DEVKIT=true</code> pour activer.
</div>
);
}
const subPage = params?.[1] || 'components';
if (subPage === 'icons') return <IconsPage />;
return <ComponentsPage />;
}
export default DevkitPage;
registerPage({ slug: 'devkit', title: 'DevKit', Component: DevkitPage });
@@ -0,0 +1,73 @@
'use client';
import { useState, useMemo } from 'react';
import * as Icons from '@zen/core/shared/icons';
import { useToast } from '@zen/core/toast';
import AdminHeader from '../components/AdminHeader.js';
const ALL_ICONS = Object.entries(Icons);
export default function IconsPage() {
const [query, setQuery] = useState('');
const toast = useToast();
const filtered = useMemo(() => {
if (!query.trim()) return ALL_ICONS;
const q = query.trim().toLowerCase();
return ALL_ICONS.filter(([name]) => name.toLowerCase().includes(q));
}, [query]);
const handleCopy = (name) => {
navigator.clipboard.writeText(`<${name} className="w-5 h-5" />`);
toast.success(`${name} copié`);
};
return (
<div className="flex flex-col gap-6">
<AdminHeader
title="Icônes"
description={`${ALL_ICONS.length} icônes disponibles`}
/>
<div className="relative">
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Rechercher une icône..."
className="w-full rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-4 py-2.5 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-blue-700/40"
/>
{query && (
<button
onClick={() => setQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200 text-lg leading-none"
>
×
</button>
)}
</div>
{filtered.length === 0 ? (
<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-12">
Aucune icône trouvée pour &ldquo;{query}&rdquo;
</p>
) : (
<div className="grid grid-cols-4 sm:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-2">
{filtered.map(([name, IconComponent]) => (
<button
key={name}
onClick={() => handleCopy(name)}
title={name}
className="flex flex-col items-center gap-1.5 rounded-lg border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 p-3 transition-colors duration-100 group cursor-pointer"
>
<IconComponent className="w-5 h-5 text-neutral-700 dark:text-neutral-300 group-hover:text-blue-700 dark:group-hover:text-blue-400 transition-colors" />
<span className="text-[9px] text-neutral-400 dark:text-neutral-500 leading-tight text-center break-all line-clamp-2 group-hover:text-neutral-600 dark:group-hover:text-neutral-300">
{name.replace('Icon', '')}
</span>
</button>
))}
</div>
)}
</div>
);
}
+7
View File
@@ -4,6 +4,7 @@ import {
getNavSections,
getNavItems,
} from './registry.js';
import { isDevkitEnabled } from '../../shared/lib/appConfig.js';
// Sections et items core — enregistrés à l'import de ce module.
registerNavSection({ id: 'dashboard', title: 'Tableau de bord', icon: 'DashboardSquare03Icon', order: 10 });
@@ -14,6 +15,12 @@ registerNavItem({ id: 'users', label: 'Utilisateurs', icon: 'UserMultiple0
registerNavItem({ id: 'roles', label: 'Rôles', icon: 'Crown03Icon', href: '/admin/roles', sectionId: 'system', order: 20 });
registerNavItem({ id: 'settings', label: 'Paramètres', icon: 'Settings02Icon', href: '/admin/settings', position: 'bottom', order: 10 });
if (isDevkitEnabled()) {
registerNavSection({ id: 'devkit', title: 'DevKit', icon: 'Wrench01Icon', order: 90 });
registerNavItem({ id: 'devkit-components', label: 'Composants', icon: 'Layers01Icon', href: '/admin/devkit/components', sectionId: 'devkit', order: 10 });
registerNavItem({ id: 'devkit-icons', label: 'Icônes', icon: 'Image01Icon', href: '/admin/devkit/icons', sectionId: 'devkit', order: 20 });
}
/**
* Build sections for AdminSidebar. Items are sérialisables (pas de composants),
* icônes en chaînes résolues côté client.
+1 -1
View File
@@ -27,7 +27,7 @@ export * as pdf from "./core/pdf/index.js";
// Do not export here to avoid mixing client/server boundaries
// Export app configuration utilities
export { getAppName, getAppConfig, getSessionCookieName, getPublicBaseUrl } from "./shared/lib/appConfig.js";
export { getAppName, getAppConfig, getSessionCookieName, getPublicBaseUrl, isDevkitEnabled } from "./shared/lib/appConfig.js";
// Export initialization utilities
export { initializeZen, resetZenInitialization } from "./shared/lib/init.js";
+4
View File
@@ -48,3 +48,7 @@ export function getAppConfig() {
currencySymbol: process.env.ZEN_CURRENCY_SYMBOL || '$',
};
}
export function isDevkitEnabled() {
return process.env.ZEN_DEVKIT === 'true';
}