Compare commits
2 Commits
345218641c
...
50f04f762b
| Author | SHA1 | Date | |
|---|---|---|---|
| 50f04f762b | |||
| 970092fccb |
@@ -50,3 +50,6 @@ ZEN_PUBLIC_LOGO_URL=
|
|||||||
|
|
||||||
# OTHERS
|
# OTHERS
|
||||||
NEXT_TELEMETRY_DISABLED=1
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# DEVKIT (developer tools)
|
||||||
|
ZEN_DEVKIT=false
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@zen/core",
|
"name": "@zen/core",
|
||||||
"version": "1.4.95",
|
"version": "1.4.96",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@zen/core",
|
"name": "@zen/core",
|
||||||
"version": "1.4.95",
|
"version": "1.4.96",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.0.0",
|
"@headlessui/react": "^2.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@zen/core",
|
"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.",
|
"description": "Une plateforme multi-usage construite sur l'essentiel, rien de plus, rien de moins.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import './pages/ProfilePage.client.js';
|
|||||||
import './pages/SettingsPage.client.js';
|
import './pages/SettingsPage.client.js';
|
||||||
import './pages/ConfirmEmailChangePage.client.js';
|
import './pages/ConfirmEmailChangePage.client.js';
|
||||||
import './widgets/index.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 parts = params?.admin || [];
|
||||||
const [first] = parts;
|
const [first] = parts;
|
||||||
|
|
||||||
@@ -25,5 +26,8 @@ export default function AdminPageClient({ params, user, widgetData, appConfig })
|
|||||||
if (slug === 'settings') {
|
if (slug === 'settings') {
|
||||||
return <Component user={user} appConfig={appConfig} />;
|
return <Component user={user} appConfig={appConfig} />;
|
||||||
}
|
}
|
||||||
|
if (slug === 'devkit') {
|
||||||
|
return <Component user={user} params={parts} devkitEnabled={devkitEnabled} />;
|
||||||
|
}
|
||||||
return <Component user={user} params={parts} />;
|
return <Component user={user} params={parts} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import AdminPageClient from './AdminPage.client.js';
|
|||||||
import { protectAdmin } from './protect.js';
|
import { protectAdmin } from './protect.js';
|
||||||
import { collectWidgetData } from './registry.js';
|
import { collectWidgetData } from './registry.js';
|
||||||
import { getAppConfig, getPublicBaseUrl } from '@zen/core';
|
import { getAppConfig, getPublicBaseUrl } from '@zen/core';
|
||||||
|
import { isDevkitEnabled } from '../../shared/lib/appConfig.js';
|
||||||
|
|
||||||
export default async function AdminPage({ params }) {
|
export default async function AdminPage({ params }) {
|
||||||
const resolvedParams = await params;
|
const resolvedParams = await params;
|
||||||
const session = await protectAdmin();
|
const session = await protectAdmin();
|
||||||
const widgetData = await collectWidgetData();
|
const widgetData = await collectWidgetData();
|
||||||
const appConfig = { ...getAppConfig(), siteUrl: getPublicBaseUrl() };
|
const appConfig = { ...getAppConfig(), siteUrl: getPublicBaseUrl() };
|
||||||
|
const devkitEnabled = isDevkitEnabled();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminPageClient
|
<AdminPageClient
|
||||||
@@ -15,6 +17,7 @@ export default async function AdminPage({ params }) {
|
|||||||
user={session.user}
|
user={session.user}
|
||||||
widgetData={widgetData}
|
widgetData={widgetData}
|
||||||
appConfig={appConfig}
|
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 “{query}”
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
getNavSections,
|
getNavSections,
|
||||||
getNavItems,
|
getNavItems,
|
||||||
} from './registry.js';
|
} from './registry.js';
|
||||||
|
import { isDevkitEnabled } from '../../shared/lib/appConfig.js';
|
||||||
|
|
||||||
// Sections et items core — enregistrés à l'import de ce module.
|
// Sections et items core — enregistrés à l'import de ce module.
|
||||||
registerNavSection({ id: 'dashboard', title: 'Tableau de bord', icon: 'DashboardSquare03Icon', order: 10 });
|
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: '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 });
|
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),
|
* Build sections for AdminSidebar. Items are sérialisables (pas de composants),
|
||||||
* icônes en chaînes résolues côté client.
|
* icônes en chaînes résolues côté client.
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export * as pdf from "./core/pdf/index.js";
|
|||||||
// Do not export here to avoid mixing client/server boundaries
|
// Do not export here to avoid mixing client/server boundaries
|
||||||
|
|
||||||
// Export app configuration utilities
|
// 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 initialization utilities
|
||||||
export { initializeZen, resetZenInitialization } from "./shared/lib/init.js";
|
export { initializeZen, resetZenInitialization } from "./shared/lib/init.js";
|
||||||
|
|||||||
@@ -48,3 +48,7 @@ export function getAppConfig() {
|
|||||||
currencySymbol: process.env.ZEN_CURRENCY_SYMBOL || '$',
|
currencySymbol: process.env.ZEN_CURRENCY_SYMBOL || '$',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDevkitEnabled() {
|
||||||
|
return process.env.ZEN_DEVKIT === 'true';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user